add hget (no ftp support)
diff --git a/src/cmd/hget.c b/src/cmd/hget.c
new file mode 100644
index 0000000..6c1986d
--- /dev/null
+++ b/src/cmd/hget.c
@@ -0,0 +1,1484 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <bio.h>
+#include <ip.h>
+#include <libsec.h>
+#include <auth.h>
+#include <thread.h>
+
+typedef struct URL URL;
+struct URL
+{
+	int	method;
+	char	*host;
+	char	*port;
+	char	*page;
+	char	*etag;
+	char	*redirect;
+	char	*postbody;
+	char	*cred;
+	long	mtime;
+};
+
+typedef struct Range Range;
+struct Range
+{
+	long	start;	/* only 2 gig supported, tdb */
+	long	end;
+};
+
+typedef struct Out Out;
+struct Out
+{
+	int fd;
+	int offset;				/* notional current offset in output */
+	int written;			/* number of bytes successfully transferred to output */
+	DigestState *curr;		/* digest state up to offset (if known) */
+	DigestState *hiwat;		/* digest state of all bytes written */
+};
+
+enum
+{
+	Http,
+	Https,
+	Ftp,
+	Other
+};
+
+enum
+{
+	Eof = 0,
+	Error = -1,
+	Server = -2,
+	Changed = -3,
+};
+
+int debug;
+char *ofile;
+
+
+int	doftp(URL*, URL*, Range*, Out*, long);
+int	dohttp(URL*, URL*,  Range*, Out*, long);
+int	crackurl(URL*, char*);
+Range*	crackrange(char*);
+int	getheader(int, char*, int);
+int	httpheaders(int, int, URL*, Range*);
+int	httprcode(int);
+int	cistrncmp(char*, char*, int);
+int	cistrcmp(char*, char*);
+void	initibuf(void);
+int	readline(int, char*, int);
+int	readibuf(int, char*, int);
+int	dfprint(int, char*, ...);
+void	unreadline(char*);
+int	output(Out*, char*, int);
+void	setoffset(Out*, int);
+
+int	verbose;
+char	*net;
+char	tcpdir[NETPATHLEN];
+int	headerprint;
+
+struct {
+	char	*name;
+	int	(*f)(URL*, URL*, Range*, Out*, long);
+} method[] = {
+	[Http]	{ "http",	dohttp },
+	[Https]	{ "https",	dohttp },
+	[Ftp]	{ "ftp",	doftp },
+	[Other]	{ "_______",	nil },
+};
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s [-hv] [-o outfile] [-p body] [-x netmtpt] url\n", argv0);
+	threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+	URL u;
+	Range r;
+	int errs, n;
+	ulong mtime;
+	Dir *d;
+	char postbody[4096], *p, *e, *t, *hpx;
+	URL px; // Proxy
+	Out out;
+
+	ofile = nil;
+	p = postbody;
+	e = p + sizeof(postbody);
+	r.start = 0;
+	r.end = -1;
+	mtime = 0;
+	memset(&u, 0, sizeof(u));
+	memset(&px, 0, sizeof(px));
+	hpx = getenv("httpproxy");
+
+	ARGBEGIN {
+	case 'o':
+		ofile = ARGF();
+		break;
+	case 'd':
+		debug = 1;
+		break;
+	case 'h':
+		headerprint = 1;
+		break;
+	case 'v':
+		verbose = 1;
+		break;
+	case 'x':
+		net = ARGF();
+		if(net == nil)
+			usage();
+		break;
+	case 'p':
+		t = ARGF();
+		if(t == nil)
+			usage();
+		if(p != postbody)
+			p = seprint(p, e, "&%s", t);
+		else
+			p = seprint(p, e, "%s", t);
+		u.postbody = postbody;
+		
+		break;
+	default:
+		usage();
+	} ARGEND;
+
+	if(net != nil){
+		if(strlen(net) > sizeof(tcpdir)-5)
+			sysfatal("network mount point too long");
+		snprint(tcpdir, sizeof(tcpdir), "%s/tcp", net);
+	} else
+		snprint(tcpdir, sizeof(tcpdir), "tcp");
+
+	if(argc != 1)
+		usage();
+
+	
+	out.fd = 1;
+	out.written = 0;
+	out.offset = 0;
+	out.curr = nil;
+	out.hiwat = nil;
+	if(ofile != nil){
+		d = dirstat(ofile);
+		if(d == nil){
+			out.fd = create(ofile, OWRITE, 0664);
+			if(out.fd < 0)
+				sysfatal("creating %s: %r", ofile);
+		} else {
+			out.fd = open(ofile, OWRITE);
+			if(out.fd < 0)
+				sysfatal("can't open %s: %r", ofile);
+			r.start = d->length;
+			mtime = d->mtime;
+			free(d);
+		}
+	}
+
+	errs = 0;
+
+	if(crackurl(&u, argv[0]) < 0)
+		sysfatal("%r");
+	if(hpx && crackurl(&px, hpx) < 0)
+		sysfatal("%r");
+
+	for(;;){
+		setoffset(&out, 0);
+		/* transfer data */
+		werrstr("");
+		n = (*method[u.method].f)(&u, &px, &r, &out, mtime);
+
+		switch(n){
+		case Eof:
+			threadexitsall(0);
+			break;
+		case Error:
+			if(errs++ < 10)
+				continue;
+			sysfatal("too many errors with no progress %r");
+			break;
+		case Server:
+			sysfatal("server returned: %r");
+			break;
+		}
+
+		/* forward progress */
+		errs = 0;
+		r.start += n;
+		if(r.start >= r.end)
+			break;
+	}
+
+	threadexitsall(0);
+}
+
+int
+crackurl(URL *u, char *s)
+{
+	char *p;
+	int i;
+
+	if(u->host != nil){
+		free(u->host);
+		u->host = nil;
+	}
+	if(u->page != nil){
+		free(u->page);
+		u->page = nil;
+	}
+
+	/* get type */
+	u->method = Other;
+	for(p = s; *p; p++){
+		if(*p == '/'){
+			u->method = Http;
+			p = s;
+			break;
+		}
+		if(*p == ':' && *(p+1)=='/' && *(p+2)=='/'){
+			*p = 0;
+			p += 3;
+			for(i = 0; i < nelem(method); i++){
+				if(cistrcmp(s, method[i].name) == 0){
+					u->method = i;
+					break;
+				}
+			}
+			break;
+		}
+	}
+
+	if(u->method == Other){
+		werrstr("unsupported URL type %s", s);
+		return -1;
+	}
+
+	/* get system */
+	s = p;
+	p = strchr(s, '/');
+	if(p == nil){
+		u->host = strdup(s);
+		u->page = strdup("/");
+	} else {
+		u->page = strdup(p);
+		*p = 0;
+		u->host = strdup(s);
+		*p = '/';
+	}
+
+	if(p = strchr(u->host, ':')) {
+		*p++ = 0;
+		u->port = p;
+	} else 
+		u->port = method[u->method].name;
+
+	if(*(u->host) == 0){
+		werrstr("bad url, null host");
+		return -1;
+	}
+
+	return 0;
+}
+
+char *day[] = {
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+char *month[] = {
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+struct
+{
+	int	fd;
+	long	mtime;
+} note;
+
+void
+catch(void *v, char *s)
+{
+	Dir d;
+
+	USED(v);
+	USED(s);
+
+	nulldir(&d);
+	d.mtime = note.mtime;
+	if(dirfwstat(note.fd, &d) < 0)
+		sysfatal("catch: can't dirfwstat: %r");
+	noted(NDFLT);
+}
+
+int
+dohttp(URL *u, URL *px, Range *r, Out *out, long mtime)
+{
+	int fd, cfd;
+	int redirect, auth, loop;
+	int n, rv, code;
+	long tot, vtime;
+	Tm *tm;
+	char buf[1024];
+	char err[ERRMAX];
+
+
+	/*  always move back to a previous 512 byte bound because some
+	 *  servers can't seem to deal with requests that start at the
+	 *  end of the file
+	 */
+	if(r->start)
+		r->start = ((r->start-1)/512)*512;
+
+	/* loop for redirects, requires reading both response code and headers */
+	fd = -1;
+	for(loop = 0; loop < 32; loop++){
+		if(px->host == nil){
+			fd = dial(netmkaddr(u->host, tcpdir, u->port), 0, 0, 0);
+		} else {
+			fd = dial(netmkaddr(px->host, tcpdir, px->port), 0, 0, 0);
+		}
+		if(fd < 0)
+			return Error;
+
+		if(u->method == Https){
+			int tfd;
+			TLSconn conn;
+
+			memset(&conn, 0, sizeof conn);
+			tfd = tlsClient(fd, &conn);
+			if(tfd < 0){
+				fprint(2, "tlsClient: %r\n");
+				close(fd);
+				return Error;
+			}
+			/* BUG: check cert here? */
+			if(conn.cert)
+				free(conn.cert);
+			close(fd);
+			fd = tfd;
+		}
+
+		/* write request, use range if not start of file */
+		if(u->postbody == nil){
+			if(px->host == nil){
+				dfprint(fd,	"GET %s HTTP/1.0\r\n"
+						"Host: %s\r\n"
+						"User-agent: Plan9/hget\r\n"
+						"Cache-Control: no-cache\r\n"
+						"Pragma: no-cache\r\n",
+						u->page, u->host);
+			} else {
+				dfprint(fd,	"GET http://%s%s HTTP/1.0\r\n"
+						"Host: %s\r\n"
+						"User-agent: Plan9/hget\r\n"
+						"Cache-Control: no-cache\r\n"
+						"Pragma: no-cache\r\n",
+						u->host, u->page, u->host);
+			}
+			if(u->cred)
+				dfprint(fd,	"Authorization: Basic %s\r\n",
+						u->cred);
+		} else {
+			dfprint(fd,	"POST %s HTTP/1.0\r\n"
+					"Host: %s\r\n"
+					"Content-type: application/x-www-form-urlencoded\r\n"
+					"Content-length: %d\r\n"
+					"User-agent: Plan9/hget\r\n"
+					"\r\n",
+					u->page, u->host, strlen(u->postbody));
+			dfprint(fd,	"%s", u->postbody);
+		}
+		if(r->start != 0){
+			dfprint(fd, "Range: bytes=%d-\n", r->start);
+			if(u->etag != nil){
+				dfprint(fd, "If-range: %s\n", u->etag);
+			} else {
+				tm = gmtime(mtime);
+				dfprint(fd, "If-range: %s, %d %s %d %2d:%2.2d:%2.2d GMT\n",
+					day[tm->wday], tm->mday, month[tm->mon],
+					tm->year+1900, tm->hour, tm->min, tm->sec);
+			}
+		}
+		if((cfd = open("/mnt/webcookies/http", ORDWR)) >= 0){
+			if(fprint(cfd, "http://%s%s", u->host, u->page) > 0){
+				while((n = read(cfd, buf, sizeof buf)) > 0){
+					if(debug)
+						write(2, buf, n);
+					write(fd, buf, n);
+				}
+			}else{
+				close(cfd);
+				cfd = -1;
+			}
+		}
+			
+		dfprint(fd, "\r\n", u->host);
+
+		auth = 0;
+		redirect = 0;
+		initibuf();
+		code = httprcode(fd);
+		switch(code){
+		case Error:	/* connection timed out */
+		case Eof:
+			close(fd);
+			close(cfd);
+			return code;
+
+		case 200:	/* OK */
+		case 201:	/* Created */
+		case 202:	/* Accepted */
+			if(ofile == nil && r->start != 0)
+				sysfatal("page changed underfoot");
+			break;
+
+		case 204:	/* No Content */
+			sysfatal("No Content");
+
+		case 206:	/* Partial Content */
+			setoffset(out, r->start);
+			break;
+
+		case 301:	/* Moved Permanently */
+		case 302:	/* Moved Temporarily */
+			redirect = 1;
+			u->postbody = nil;
+			break;
+
+		case 304:	/* Not Modified */
+			break;
+
+		case 400:	/* Bad Request */
+			sysfatal("Bad Request");
+
+		case 401:	/* Unauthorized */
+			if (auth)
+				sysfatal("Authentication failed");
+			auth = 1;
+			break;
+
+		case 402:	/* ??? */
+			sysfatal("Unauthorized");
+
+		case 403:	/* Forbidden */
+			sysfatal("Forbidden by server");
+
+		case 404:	/* Not Found */
+			sysfatal("Not found on server");
+
+		case 407:	/* Proxy Authentication */
+			sysfatal("Proxy authentication required");
+
+		case 500:	/* Internal server error */
+			sysfatal("Server choked");
+
+		case 501:	/* Not implemented */
+			sysfatal("Server can't do it!");
+
+		case 502:	/* Bad gateway */
+			sysfatal("Bad gateway");
+
+		case 503:	/* Service unavailable */
+			sysfatal("Service unavailable");
+		
+		default:
+			sysfatal("Unknown response code %d", code);
+		}
+
+		if(u->redirect != nil){
+			free(u->redirect);
+			u->redirect = nil;
+		}
+
+		rv = httpheaders(fd, cfd, u, r);
+		close(cfd);
+		if(rv != 0){
+			close(fd);
+			return rv;
+		}
+
+		if(!redirect && !auth)
+			break;
+
+		if (redirect){
+			if(u->redirect == nil)
+				sysfatal("redirect: no URL");
+			if(crackurl(u, u->redirect) < 0)
+				sysfatal("redirect: %r");
+		}
+	}
+
+	/* transfer whatever you get */
+	if(ofile != nil && u->mtime != 0){
+		note.fd = out->fd;
+		note.mtime = u->mtime;
+		notify(catch);
+	}
+
+	tot = 0;
+	vtime = 0;
+	for(;;){
+		n = readibuf(fd, buf, sizeof(buf));
+		if(n <= 0)
+			break;
+		if(output(out, buf, n) != n)
+			break;
+		tot += n;
+		if(verbose && vtime != time(0)) {
+			vtime = time(0);
+			fprint(2, "%ld %ld\n", r->start+tot, r->end);
+		}
+	}
+	notify(nil);
+	close(fd);
+
+	if(ofile != nil && u->mtime != 0){
+		Dir d;
+
+		rerrstr(err, sizeof err);
+		nulldir(&d);
+		d.mtime = u->mtime;
+		if(dirfwstat(out->fd, &d) < 0)
+			fprint(2, "couldn't set mtime: %r\n");
+		errstr(err, sizeof err);
+	}
+
+	return tot;
+}
+
+/* get the http response code */
+int
+httprcode(int fd)
+{
+	int n;
+	char *p;
+	char buf[256];
+
+	n = readline(fd, buf, sizeof(buf)-1);
+	if(n <= 0)
+		return n;
+	if(debug)
+		fprint(2, "%d <- %s\n", fd, buf);
+	p = strchr(buf, ' ');
+	if(strncmp(buf, "HTTP/", 5) != 0 || p == nil){
+		werrstr("bad response from server");
+		return -1;
+	}
+	buf[n] = 0;
+	return atoi(p+1);
+}
+
+/* read in and crack the http headers, update u and r */
+void	hhetag(char*, URL*, Range*);
+void	hhmtime(char*, URL*, Range*);
+void	hhclen(char*, URL*, Range*);
+void	hhcrange(char*, URL*, Range*);
+void	hhuri(char*, URL*, Range*);
+void	hhlocation(char*, URL*, Range*);
+void	hhauth(char*, URL*, Range*);
+
+struct {
+	char *name;
+	void (*f)(char*, URL*, Range*);
+} headers[] = {
+	{ "etag:", hhetag },
+	{ "last-modified:", hhmtime },
+	{ "content-length:", hhclen },
+	{ "content-range:", hhcrange },
+	{ "uri:", hhuri },
+	{ "location:", hhlocation },
+	{ "WWW-Authenticate:", hhauth },
+};
+int
+httpheaders(int fd, int cfd, URL *u, Range *r)
+{
+	char buf[2048];
+	char *p;
+	int i, n;
+
+	for(;;){
+		n = getheader(fd, buf, sizeof(buf));
+		if(n <= 0)
+			break;
+		if(cfd >= 0)
+			fprint(cfd, "%s\n", buf);
+		for(i = 0; i < nelem(headers); i++){
+			n = strlen(headers[i].name);
+			if(cistrncmp(buf, headers[i].name, n) == 0){
+				/* skip field name and leading white */
+				p = buf + n;
+				while(*p == ' ' || *p == '\t')
+					p++;
+
+				(*headers[i].f)(p, u, r);
+				break;
+			}
+		}
+	}
+	return n;
+}
+
+/*
+ *  read a single mime header, collect continuations.
+ *
+ *  this routine assumes that there is a blank line twixt
+ *  the header and the message body, otherwise bytes will
+ *  be lost.
+ */
+int
+getheader(int fd, char *buf, int n)
+{
+	char *p, *e;
+	int i;
+
+	n--;
+	p = buf;
+	for(e = p + n; ; p += i){
+		i = readline(fd, p, e-p);
+		if(i < 0)
+			return i;
+
+		if(p == buf){
+			/* first line */
+			if(strchr(buf, ':') == nil)
+				break;		/* end of headers */
+		} else {
+			/* continuation line */
+			if(*p != ' ' && *p != '\t'){
+				unreadline(p);
+				*p = 0;
+				break;		/* end of this header */
+			}
+		}
+	}
+	if(headerprint)
+		print("%s\n", buf);
+
+	if(debug)
+		fprint(2, "%d <- %s\n", fd, buf);
+	return p-buf;
+}
+
+void
+hhetag(char *p, URL *u, Range *r)
+{
+	USED(r);
+	
+	if(u->etag != nil){
+		if(strcmp(u->etag, p) != 0)
+			sysfatal("file changed underfoot");
+	} else
+		u->etag = strdup(p);
+}
+
+char*	monthchars = "janfebmaraprmayjunjulaugsepoctnovdec";
+
+void
+hhmtime(char *p, URL *u, Range *r)
+{
+	char *month, *day, *yr, *hms;
+	char *fields[6];
+	Tm tm, now;
+	int i;
+
+	USED(r);
+	
+	i = getfields(p, fields, 6, 1, " \t");
+	if(i < 5)
+		return;
+
+	day = fields[1];
+	month = fields[2];
+	yr = fields[3];
+	hms = fields[4];
+
+	/* default time */
+	now = *gmtime(time(0));
+	tm = now;
+	tm.yday = 0;
+
+	/* convert ascii month to a number twixt 1 and 12 */
+	if(*month >= '0' && *month <= '9'){
+		tm.mon = atoi(month) - 1;
+		if(tm.mon < 0 || tm.mon > 11)
+			tm.mon = 5;
+	} else {
+		for(p = month; *p; p++)
+			*p = tolower(*p);
+		for(i = 0; i < 12; i++)
+			if(strncmp(&monthchars[i*3], month, 3) == 0){
+				tm.mon = i;
+				break;
+			}
+	}
+
+	tm.mday = atoi(day);
+
+	if(hms) {
+		tm.hour = strtoul(hms, &p, 10);
+		if(*p == ':') {
+			p++;
+			tm.min = strtoul(p, &p, 10);
+			if(*p == ':') {
+				p++;
+				tm.sec = strtoul(p, &p, 10);
+			}
+		}
+		if(tolower(*p) == 'p')
+			tm.hour += 12;
+	}
+
+	if(yr) {
+		tm.year = atoi(yr);
+		if(tm.year >= 1900)
+			tm.year -= 1900;
+	} else {
+		if(tm.mon > now.mon || (tm.mon == now.mon && tm.mday > now.mday+1))
+			tm.year--;
+	}
+
+	strcpy(tm.zone, "GMT");
+	/* convert to epoch seconds */
+	u->mtime = tm2sec(&tm);
+}
+
+void
+hhclen(char *p, URL *u, Range *r)
+{
+	USED(u);
+	
+	r->end = atoi(p);
+}
+
+void
+hhcrange(char *p, URL *u, Range *r)
+{
+	char *x;
+	vlong l;
+
+	USED(u);
+	l = 0;
+	x = strchr(p, '/');
+	if(x)
+		l = atoll(x+1);
+	if(l == 0)
+	x = strchr(p, '-');
+	if(x)
+		l = atoll(x+1);
+	if(l)
+		r->end = l;
+}
+
+void
+hhuri(char *p, URL *u, Range *r)
+{
+	USED(r);
+	
+	if(*p != '<')
+		return;
+	u->redirect = strdup(p+1);
+	p = strchr(u->redirect, '>');
+	if(p != nil)
+		*p = 0;
+}
+
+void
+hhlocation(char *p, URL *u, Range *r)
+{
+	USED(r);
+	
+	u->redirect = strdup(p);
+}
+
+void
+hhauth(char *p, URL *u, Range *r)
+{
+	char *f[4];
+	UserPasswd *up;
+	char *s, cred[64];
+	
+	USED(r);
+
+	if (cistrncmp(p, "basic ", 6) != 0)
+		sysfatal("only Basic authentication supported");
+
+	if (gettokens(p, f, nelem(f), "\"") < 2)
+		sysfatal("garbled auth data");
+
+	if ((up = auth_getuserpasswd(auth_getkey, "proto=pass service=http dom=%q relm=%q",
+	    	u->host, f[1])) == nil)
+			sysfatal("cannot authenticate");
+
+	s = smprint("%s:%s", up->user, up->passwd);
+	if(enc64(cred, sizeof(cred), (uchar *)s, strlen(s)) == -1)
+		sysfatal("enc64");
+  		free(s);
+
+	assert(u->cred = strdup(cred));
+}
+
+enum
+{
+	/* ftp return codes */
+	Extra=		1,
+	Success=	2,
+	Incomplete=	3,
+	TempFail=	4,
+	PermFail=	5,
+
+	Nnetdir=	64,	/* max length of network directory paths */
+	Ndialstr=	64,		/* max length of dial strings */
+};
+
+int ftpcmd(int, char*, ...);
+int ftprcode(int, char*, int);
+int hello(int);
+int logon(int);
+int xfertype(int, char*);
+int passive(int, URL*);
+int active(int, URL*);
+int ftpxfer(int, Out*, Range*);
+int terminateftp(int, int);
+int getaddrport(char*, uchar*, uchar*);
+int ftprestart(int, Out*, URL*, Range*, long);
+
+int
+doftp(URL *u, URL *px, Range *r, Out *out, long mtime)
+{
+	int pid, ctl, data, rv;
+	Waitmsg *w;
+	char msg[64];
+	char conndir[NETPATHLEN];
+	char *p;
+
+	/* untested, proxy dosn't work with ftp (I think) */
+	if(px->host == nil){
+		ctl = dial(netmkaddr(u->host, tcpdir, u->port), 0, conndir, 0);
+	} else {
+		ctl = dial(netmkaddr(px->host, tcpdir, px->port), 0, conndir, 0);
+	}
+
+	if(ctl < 0)
+		return Error;
+	if(net == nil){
+		p = strrchr(conndir, '/');
+		*p = 0;
+		snprint(tcpdir, sizeof(tcpdir), conndir);
+	}
+
+	initibuf();
+
+	rv = hello(ctl);
+	if(rv < 0)
+		return terminateftp(ctl, rv);
+
+	rv = logon(ctl);
+	if(rv < 0)
+		return terminateftp(ctl, rv);
+
+	rv = xfertype(ctl, "I");
+	if(rv < 0)
+		return terminateftp(ctl, rv);
+
+	/* if file is up to date and the right size, stop */
+	if(ftprestart(ctl, out, u, r, mtime) > 0){
+		close(ctl);
+		return Eof;
+	}
+		
+	/* first try passive mode, then active */
+	data = passive(ctl, u);
+	if(data < 0){
+		data = active(ctl, u);
+		if(data < 0)
+			return Error;
+	}
+
+	/* fork */
+	switch(pid = fork()){
+	case -1:
+		close(data);
+		return terminateftp(ctl, Error);
+	case 0:
+		ftpxfer(data, out, r);
+		close(data);
+		#undef _exits
+		_exits(0);
+	default:
+		close(data);
+		break;
+	}
+
+	/* wait for reply message */
+	rv = ftprcode(ctl, msg, sizeof(msg));
+	close(ctl);
+
+	/* wait for process to terminate */
+	w = nil;
+	for(;;){
+		free(w);
+		w = wait();
+		if(w == nil)
+			return Error;
+		if(w->pid == pid){
+			if(w->msg[0] == 0){
+				free(w);
+				break;
+			}
+			werrstr("xfer: %s", w->msg);
+			free(w);
+			return Error;
+		}
+	}
+
+	switch(rv){
+	case Success:
+		return Eof;
+	case TempFail:
+		return Server;
+	default:
+		return Error;
+	}
+}
+
+int
+ftpcmd(int ctl, char *fmt, ...)
+{
+	va_list arg;
+	char buf[2*1024], *s;
+
+	va_start(arg, fmt);
+	s = vseprint(buf, buf + (sizeof(buf)-4) / sizeof(*buf), fmt, arg);
+	va_end(arg);
+	if(debug)
+		fprint(2, "%d -> %s\n", ctl, buf);
+	*s++ = '\r';
+	*s++ = '\n';
+	if(write(ctl, buf, s - buf) != s - buf)
+		return -1;
+	return 0;
+}
+
+int
+ftprcode(int ctl, char *msg, int len)
+{
+	int rv;
+	int i;
+	char *p;
+
+	len--;	/* room for terminating null */
+	for(;;){
+		*msg = 0;
+		i = readline(ctl, msg, len);
+		if(i < 0)
+			break;
+		if(debug)
+			fprint(2, "%d <- %s\n", ctl, msg);
+
+		/* stop if not a continuation */
+		rv = strtol(msg, &p, 10);
+		if(rv >= 100 && rv < 600 && p==msg+3 && *p == ' ')
+			return rv/100;
+	}
+	*msg = 0;
+
+	return -1;
+}
+
+int
+hello(int ctl)
+{
+	char msg[1024];
+
+	/* wait for hello from other side */
+	if(ftprcode(ctl, msg, sizeof(msg)) != Success){
+		werrstr("HELLO: %s", msg);
+		return Server;
+	}
+	return 0;
+}
+
+int
+getdec(char *p, int n)
+{
+	int x = 0;
+	int i;
+
+	for(i = 0; i < n; i++)
+		x = x*10 + (*p++ - '0');
+	return x;
+}
+
+int
+ftprestart(int ctl, Out *out, URL *u, Range *r, long mtime)
+{
+	Tm tm;
+	char msg[1024];
+	long x, rmtime;
+
+	ftpcmd(ctl, "MDTM %s", u->page);
+	if(ftprcode(ctl, msg, sizeof(msg)) != Success){
+		r->start = 0;
+		return 0;		/* need to do something */
+	}
+
+	/* decode modification time */
+	if(strlen(msg) < 4 + 4 + 2 + 2 + 2 + 2 + 2){
+		r->start = 0;
+		return 0;		/* need to do something */
+	}
+	memset(&tm, 0, sizeof(tm));
+	tm.year = getdec(msg+4, 4) - 1900;
+	tm.mon = getdec(msg+4+4, 2) - 1;
+	tm.mday = getdec(msg+4+4+2, 2);
+	tm.hour = getdec(msg+4+4+2+2, 2);
+	tm.min = getdec(msg+4+4+2+2+2, 2);
+	tm.sec = getdec(msg+4+4+2+2+2+2, 2);
+	strcpy(tm.zone, "GMT");
+	rmtime = tm2sec(&tm);
+	if(rmtime > mtime)
+		r->start = 0;
+
+	/* get size */
+	ftpcmd(ctl, "SIZE %s", u->page);
+	if(ftprcode(ctl, msg, sizeof(msg)) == Success){
+		x = atol(msg+4);
+		if(r->start == x)
+			return 1;	/* we're up to date */
+		r->end = x;
+	}
+
+	/* seek to restart point */
+	if(r->start > 0){
+		ftpcmd(ctl, "REST %lud", r->start);
+		if(ftprcode(ctl, msg, sizeof(msg)) == Incomplete){
+			setoffset(out, r->start);
+		}else
+			r->start = 0;
+	}
+
+	return 0;	/* need to do something */
+}
+
+int
+logon(int ctl)
+{
+	char msg[1024];
+
+	/* login anonymous */
+	ftpcmd(ctl, "USER anonymous");
+	switch(ftprcode(ctl, msg, sizeof(msg))){
+	case Success:
+		return 0;
+	case Incomplete:
+		break;	/* need password */
+	default:
+		werrstr("USER: %s", msg);
+		return Server;
+	}
+
+	/* send user id as password */
+	sprint(msg, "%s@closedmind.org", getuser());
+	ftpcmd(ctl, "PASS %s", msg);
+	if(ftprcode(ctl, msg, sizeof(msg)) != Success){
+		werrstr("PASS: %s", msg);
+		return Server;
+	}
+
+	return 0;
+}
+
+int
+xfertype(int ctl, char *t)
+{
+	char msg[1024];
+
+	ftpcmd(ctl, "TYPE %s", t);
+	if(ftprcode(ctl, msg, sizeof(msg)) != Success){
+		werrstr("TYPE %s: %s", t, msg);
+		return Server;
+	}
+
+	return 0;
+}
+
+int
+passive(int ctl, URL *u)
+{
+	char msg[1024];
+	char ipaddr[32];
+	char *f[6];
+	char *p;
+	int fd;
+	int port;
+	char aport[12];
+
+	ftpcmd(ctl, "PASV");
+	if(ftprcode(ctl, msg, sizeof(msg)) != Success)
+		return Error;
+
+	/* get address and port number from reply, this is AI */
+	p = strchr(msg, '(');
+	if(p == nil){
+		for(p = msg+3; *p; p++)
+			if(isdigit(*p))
+				break;
+	} else
+		p++;
+	if(getfields(p, f, 6, 0, ",)") < 6){
+		werrstr("ftp protocol botch");
+		return Server;
+	}
+	snprint(ipaddr, sizeof(ipaddr), "%s.%s.%s.%s",
+		f[0], f[1], f[2], f[3]);
+	port = ((atoi(f[4])&0xff)<<8) + (atoi(f[5])&0xff);
+	sprint(aport, "%d", port);
+
+	/* open data connection */
+	fd = dial(netmkaddr(ipaddr, tcpdir, aport), 0, 0, 0);
+	if(fd < 0){
+		werrstr("passive mode failed: %r");
+		return Error;
+	}
+
+	/* tell remote to send a file */
+	ftpcmd(ctl, "RETR %s", u->page);
+	if(ftprcode(ctl, msg, sizeof(msg)) != Extra){
+		werrstr("RETR %s: %s", u->page, msg);
+		return Error;
+	}
+	return fd;
+}
+
+int
+active(int ctl, URL *u)
+{
+	char msg[1024];
+	char dir[40], ldir[40];
+	uchar ipaddr[4];
+	uchar port[2];
+	int lcfd, dfd, afd;
+
+	/* announce a port for the call back */
+	snprint(msg, sizeof(msg), "%s!*!0", tcpdir);
+	afd = announce(msg, dir);
+	if(afd < 0)
+		return Error;
+
+	/* get a local address/port of the annoucement */
+	if(getaddrport(dir, ipaddr, port) < 0){
+		close(afd);
+		return Error;
+	}
+
+	/* tell remote side address and port*/
+	ftpcmd(ctl, "PORT %d,%d,%d,%d,%d,%d", ipaddr[0], ipaddr[1], ipaddr[2],
+		ipaddr[3], port[0], port[1]);
+	if(ftprcode(ctl, msg, sizeof(msg)) != Success){
+		close(afd);
+		werrstr("active: %s", msg);
+		return Error;
+	}
+
+	/* tell remote to send a file */
+	ftpcmd(ctl, "RETR %s", u->page);
+	if(ftprcode(ctl, msg, sizeof(msg)) != Extra){
+		close(afd);
+		werrstr("RETR: %s", msg);
+		return Server;
+	}
+
+	/* wait for a connection */
+	lcfd = listen(dir, ldir);
+	if(lcfd < 0){
+		close(afd);
+		return Error;
+	}
+	dfd = accept(lcfd, ldir);
+	if(dfd < 0){
+		close(afd);
+		close(lcfd);
+		return Error;
+	}
+	close(afd);
+	close(lcfd);
+	
+	return dfd;
+}
+
+int
+ftpxfer(int in, Out *out, Range *r)
+{
+	char buf[1024];
+	long vtime;
+	int i, n;
+
+	vtime = 0;
+	for(n = 0;;n += i){
+		i = read(in, buf, sizeof(buf));
+		if(i == 0)
+			break;
+		if(i < 0)
+			return Error;
+		if(output(out, buf, i) != i)
+			return Error;
+		r->start += i;
+		if(verbose && vtime != time(0)) {
+			vtime = time(0);
+			fprint(2, "%ld %ld\n", r->start, r->end);
+		}
+	}
+	return n;
+}
+
+int
+terminateftp(int ctl, int rv)
+{
+	close(ctl);
+	return rv;
+}
+
+/*
+ * case insensitive strcmp (why aren't these in libc?)
+ */
+int
+cistrncmp(char *a, char *b, int n)
+{
+	while(n-- > 0){
+		if(tolower(*a++) != tolower(*b++))
+			return -1;
+	}
+	return 0;
+}
+
+int
+cistrcmp(char *a, char *b)
+{
+	while(*a || *b)
+		if(tolower(*a++) != tolower(*b++))
+			return -1;
+
+	return 0;
+}
+
+/*
+ *  buffered io
+ */
+struct
+{
+	char *rp;
+	char *wp;
+	char buf[4*1024];
+} b;
+
+void
+initibuf(void)
+{
+	b.rp = b.wp = b.buf;
+}
+
+/*
+ *  read a possibly buffered line, strip off trailing while
+ */
+int
+readline(int fd, char *buf, int len)
+{
+	int n;
+	char *p;
+	int eof = 0;
+
+	len--;
+
+	for(p = buf;;){
+		if(b.rp >= b.wp){
+			n = read(fd, b.wp, sizeof(b.buf)/2);
+			if(n < 0)
+				return -1;
+			if(n == 0){
+				eof = 1;
+				break;
+			}
+			b.wp += n;
+		}
+		n = *b.rp++;
+		if(len > 0){
+			*p++ = n;
+			len--;
+		}
+		if(n == '\n')
+			break;
+	}
+
+	/* drop trailing white */
+	for(;;){
+		if(p <= buf)
+			break;
+		n = *(p-1);
+		if(n != ' ' && n != '\t' && n != '\r' && n != '\n')
+			break;
+		p--;
+	}
+	*p = 0;
+
+	if(eof && p == buf)
+		return -1;
+
+	return p-buf;
+}
+
+void
+unreadline(char *line)
+{
+	int i, n;
+
+	i = strlen(line);
+	n = b.wp-b.rp;
+	memmove(&b.buf[i+1], b.rp, n);
+	memmove(b.buf, line, i);
+	b.buf[i] = '\n';
+	b.rp = b.buf;
+	b.wp = b.rp + i + 1 + n;
+}
+
+int
+readibuf(int fd, char *buf, int len)
+{
+	int n;
+
+	n = b.wp-b.rp;
+	if(n > 0){
+		if(n > len)
+			n = len;
+		memmove(buf, b.rp, n);
+		b.rp += n;
+		return n;
+	}
+	return read(fd, buf, len);
+}
+
+int
+dfprint(int fd, char *fmt, ...)
+{
+	char buf[4*1024];
+	va_list arg;
+
+	va_start(arg, fmt);
+	vseprint(buf, buf+sizeof(buf), fmt, arg);
+	va_end(arg);
+	if(debug)
+		fprint(2, "%d -> %s", fd, buf);
+	return fprint(fd, "%s", buf);
+}
+
+int
+getaddrport(char *dir, uchar *ipaddr, uchar *port)
+{
+	char buf[256];
+	int fd, i;
+	char *p;
+
+	snprint(buf, sizeof(buf), "%s/local", dir);
+	fd = open(buf, OREAD);
+	if(fd < 0)
+		return -1;
+	i = read(fd, buf, sizeof(buf)-1);
+	close(fd);
+	if(i <= 0)
+		return -1;
+	buf[i] = 0;
+	p = strchr(buf, '!');
+	if(p != nil)
+		*p++ = 0;
+	v4parseip(ipaddr, buf);
+	i = atoi(p);
+	port[0] = i>>8;
+	port[1] = i;
+	return 0;
+}
+
+void
+md5free(DigestState *state)
+{
+	uchar x[MD5dlen];
+	md5(nil, 0, x, state);
+}
+
+DigestState*
+md5dup(DigestState *state)
+{
+	DigestState *s2;
+	
+	s2 = malloc(sizeof(DigestState));
+	if(s2 == nil)
+		sysfatal("malloc: %r");
+	*s2 = *state;
+	s2->malloced = 1;
+	return s2;
+}
+
+void
+setoffset(Out *out, int offset)
+{
+	md5free(out->curr);
+	if(offset == 0)
+		out->curr = md5(nil, 0, nil, nil);
+	else
+		out->curr = nil;
+	out->offset = offset;
+}
+
+/*
+ * write some output, discarding it (but keeping track)
+ * if we've already written it. if we've gone backwards,
+ * verify that everything previously written matches
+ * that which would have been written from the current
+ * output.
+ */
+int
+output(Out *out, char *buf, int nb)
+{
+	int n, d;
+	uchar m0[MD5dlen], m1[MD5dlen];
+
+	n = nb;
+	d = out->written - out->offset;
+	assert(d >= 0);
+	if(d > 0){
+		if(n < d){
+			if(out->curr != nil)
+				md5((uchar*)buf, n, nil, out->curr);
+			out->offset += n;
+			return n;
+		}
+		if(out->curr != nil){
+			md5((uchar*)buf, d, m0, out->curr);
+			out->curr = nil;
+			md5(nil, 0, m1, md5dup(out->hiwat));
+			if(memcmp(m0, m1, MD5dlen) != 0){
+				fprint(2, "integrity check failure at offset %d\n", out->written);
+				return -1;
+			}
+		}
+		buf += d;
+		n -= d;
+		out->offset += d;
+	}
+	if(n > 0){
+		out->hiwat = md5((uchar*)buf, n, nil, out->hiwat);
+		n = write(out->fd, buf, n);
+		if(n > 0){
+			out->offset += n;
+			out->written += n;
+		}
+	}
+	return n + d;
+}
+