xref: /inferno-os/appl/charon/ftp.b (revision d0c0f54bdb5840c1f71d49561438fa5d3bd10ff2)
1implement Transport;
2
3include "common.m";
4include "transport.m";
5
6# local copies from CU
7sys: Sys;
8U: Url;
9	Parsedurl: import U;
10S: String;
11DI: Dial;
12CU: CharonUtils;
13	Netconn, ByteSource, Header, config: import CU;
14
15FTPPORT: con 21;
16
17# Return codes
18Extra, Success, Incomplete, TempFail, PermFail : con (1+iota);
19
20cmdbuf := array[200] of byte;
21dbg := 0;
22
23init(c: CharonUtils)
24{
25	CU = c;
26	sys = load Sys Sys->PATH;
27	S = load String String->PATH;
28	U = load Url Url->PATH;
29	if (U != nil)
30		U->init();
31	DI = CU->DI;
32	dbg = int (CU->config).dbg['n'];
33}
34
35connect(nc: ref Netconn, bs: ref ByteSource)
36{
37	port := nc.port;
38	if(port == 0)
39		port = FTPPORT;
40	addr := DI->netmkaddr(nc.host, "net", string port);
41	if(dbg)
42		sys->print("ftp %d: dialing %s\n", nc.id, addr);
43	err := "";
44	ctlfd : ref sys->FD = nil;
45	nc.conn = DI->dial(addr, nil);
46	if(nc.conn == nil) {
47		syserr := sys->sprint("%r");
48		if(S->prefix("cs: dialup", syserr))
49			err = syserr[4:];
50		else if(S->prefix("cs: dns: no translation found", syserr))
51			err = "unknown host";
52		else
53			err = sys->sprint("couldn't connect: %s", syserr);
54	}
55	else {
56		if(dbg)
57			sys->print("ftp %d: connected\n", nc.id);
58		ctlfd = nc.conn.dfd;
59		# use cfd to hold control connection so can use dfd to hold data connection
60		nc.conn.cfd = ctlfd;
61		nc.conn.dfd = nil;
62
63		# look for Hello
64		(code, msg) := getreply(nc, ctlfd);
65		if(code != Success)
66			err = "instead of hello: " + msg;
67		else {
68			# logon
69			err = sendrequest(nc, ctlfd, "USER anonymous");
70			if(err == "") {
71				(code, msg) = getreply(nc, ctlfd);
72				if(code == Incomplete) {
73					# need password
74					err = sendrequest(nc, ctlfd, "PASS webget@webget.com");
75					if(err == "")
76						(code, msg) = getreply(nc, ctlfd);
77				}
78				if(err == "") {
79					if(code != Success)
80						err =  "login failed: " + msg;
81
82					# image type
83					err = sendrequest(nc, ctlfd, "TYPE I");
84					if(err == "") {
85						(code, msg) = getreply(nc, ctlfd);
86						if(code != Success)
87							err =  "can't set type I: " + msg;
88					}
89				}
90			}
91		}
92	}
93	if(err == "") {
94		nc.connected = 1;
95		nc.state = CU->NCgethdr;
96	}
97	else {
98		if(dbg)
99			sys->print("ftp %d: connection failed: %s\n", nc.id, err);
100		bs.err = err;
101		closeconn(nc);
102	}
103}
104
105# Ask ftp server on ctlfd for passive port and dial it
106dialdata(nc: ref Netconn, ctlfd: ref sys->FD) : string
107{
108	# put in passive mode
109	sendrequest(nc, ctlfd, "PASV");
110	(code, msg) := getreply(nc, ctlfd);
111	if(code != Success)
112		return "can't use passive mode: " + msg;
113	(paddr, pport) := passvap(msg);
114	if(paddr == "")
115		return "passive mode protocol botch: " + msg;
116	# dial data port
117	daddr := DI->netmkaddr(paddr, "net", pport);
118	if(dbg)
119		sys->print("ftp %d: dialing data %s", nc.id, daddr);
120	dnet := DI->dial(daddr, nil);
121	if(dnet == nil)
122		return "data dial error";
123	nc.conn.dfd = dnet.dfd;
124	return "";
125}
126
127writereq(nc: ref Netconn, bs: ref ByteSource)
128{
129	ctlfd := nc.conn.cfd;
130	CU->assert(ctlfd != nil);
131	err := dialdata(nc, ctlfd);
132	if(err == "") {
133		# tell remote to send file
134		err = sendrequest(nc, ctlfd, "RETR " + bs.req.url.path);
135	}
136	if(err != "") {
137		if(dbg)
138			sys->print("ftp %d: error: %s\n", nc.id, err);
139		bs.err = err;
140		closeconn(nc);
141	}
142}
143
144gethdr(nc: ref Netconn, bs: ref ByteSource)
145{
146	hdr := Header.new();
147	bs.hdr = hdr;
148	err := "";
149	ctlfd := nc.conn.cfd;
150	dfd := nc.conn.dfd;
151	CU->assert(ctlfd != nil && dfd != nil);
152	(code, msg) := getreply(nc, ctlfd);
153	if(code != Extra) {
154		if(dbg)
155			sys->print("ftp %d: retrieve failed: %s\n",
156				nc.id, msg);
157		hdr.code = CU->HCNotFound;
158		hdr.msg = "Not found";
159	}
160	else {
161		hdr.code = CU->HCOk;
162
163		# try to guess media type before returning header
164		buf := array[sys->ATOMICIO] of byte;
165		n := sys->read(dfd, buf, len buf);
166		if(dbg)
167			sys->print("ftp %d: read %d bytes\n", nc.id, n);
168		if(n < 0)
169			err = "error reading data";
170		else {
171			if(n > 0)
172				nc.tbuf = buf[0:n];
173			else
174				nc.tbuf = nil;
175			hdr.setmediatype(bs.req.url.path, nc.tbuf);
176			hdr.actual = bs.req.url;
177			hdr.base = hdr.actual;
178			hdr.length = -1;
179			hdr.msg = "Ok";
180		}
181	}
182	if(err != "") {
183		if(dbg)
184			sys->print("ftp %d: error %s\n", nc.id, err);
185		bs.err = err;
186		closeconn(nc);
187	}
188}
189
190getdata(nc: ref Netconn, bs: ref ByteSource): int
191{
192	dfd := nc.conn.dfd;
193	CU->assert(dfd != nil);
194	if (bs.data == nil || bs.edata >= len bs.data) {
195		closeconn(nc);
196		return 0;
197	}
198	buf := bs.data[bs.edata:];
199	n := len buf;
200	if (nc.tbuf != nil) {
201		# initial overread of header
202		if (n >= len nc.tbuf) {
203			n = len nc.tbuf;
204			buf[:] = nc.tbuf;
205			nc.tbuf = nil;
206			return n;
207		}
208		buf[:] = nc.tbuf[:n];
209		nc.tbuf = nc.tbuf[n:];
210		return n;
211	}
212	n = sys->read(dfd, buf, n);
213	if(dbg > 1)
214		sys->print("ftp %d: read %d bytes\n", nc.id, n);
215	if(n <= 0) {
216		bs.err = "eof";
217		closeconn(nc);
218	}
219	return n;
220}
221
222# Send ftp request cmd along fd; return "" if OK else error string.
223sendrequest(nc: ref Netconn, fd: ref sys->FD, cmd: string) : string
224{
225	if(dbg > 1)
226		sys->print("ftp %d: send request: %s\n", nc.id, cmd);
227	cmd = cmd + "\r\n";
228	buf := array of byte cmd;
229	n := len buf;
230	if(sys->write(fd, buf, n) != n)
231		return sys->sprint("write error: %r");
232	return "";
233}
234
235# Get reply to ftp request along fd.
236# Reply may be more than one line ("commentary")
237# but ends with a line that has a status code in the first
238# three characters (a number between 100 and 600)
239# followed by a blank and a possible message.
240# If OK, return the hundreds digit of the status (which will
241# mean one of Extra, Success, etc.), and the whole
242# last line; else return (-1, "").
243getreply(nc: ref Netconn, fd: ref sys->FD) : (int, string)
244{
245	# Reply might contain more than one line,
246	# because there might be "commentary" lines.
247	i := 0;
248	j := 0;
249	aline: array of byte;
250	eof := 0;
251	for(;;) {
252		(aline, eof, i, j) = CU->getline(fd, cmdbuf, i, j);
253		if(eof)
254			break;
255		line := string aline;
256		n := len line;
257		if(n == 0)
258			break;
259		if(dbg > 1)
260			sys->print("ftp %d: got reply: %s\n", nc.id, line);
261		rv := int line;
262		if(rv >= 100 && rv < 600) {
263			# if line is like '123-stuff'
264			# then there will be more lines until
265			# '123 stuff'
266			if(len line<4 || line[3]==' ')
267				return (rv/100, line);
268		}
269	}
270	return (-1, "");
271}
272
273# Parse reply to PASSV to find address and port numbers.
274# This is AI because extant agents aren't good at following
275# the standard.
276passvap(s: string) : (string, string)
277{
278	addr := "";
279	port := "";
280	(nil, v) := S->splitl(s, "(");
281	if(v != "")
282		s = v[1:];
283	else
284		(nil, s) = S->splitl(s, "0123456789");
285	if(s != "") {
286		(n, l) := sys->tokenize(s, ",");
287		if(n >= 6) {
288			addr = hd l + ".";
289			l = tl l;
290			addr += hd l + ".";
291			l = tl l;
292			addr += hd l + ".";
293			l = tl l;
294			addr += hd l;
295			l = tl l;
296			p1 := int hd l;
297			p2 := int hd tl l;
298			port = string (((p1&255)<<8)|(p2&255));
299		}
300	}
301	return (addr, port);
302}
303
304defaultport(nil: string) : int
305{
306	return FTPPORT;
307}
308
309closeconn(nc: ref Netconn)
310{
311	nc.conn = nil;
312	nc.connected = 0;
313}
314