| #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", dohttp }, |
| { "https", dohttp }, |
| { "ftp", doftp }, |
| { "_______", 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 { |
| if(px->host == nil){ |
| 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)); |
| } else { |
| dfprint(fd, "POST http://%s%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->host, 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) || r->start == r->end)) { |
| 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((uchar)*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((uchar)*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]; |
| |
| /* untested, proxy dosn't work with ftp (I think) */ |
| if(px->host == nil){ |
| ctl = dial(netmkaddr(u->host, tcpdir, u->port), 0, 0, 0); |
| } else { |
| ctl = dial(netmkaddr(px->host, tcpdir, px->port), 0, 0, 0); |
| } |
| |
| if(ctl < 0) |
| return Error; |
| if(net == nil) |
| strcpy(tcpdir, "tcp"); |
| |
| 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((uchar)*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) || r->start == r->end)) { |
| 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((uchar)*a++) != tolower((uchar)*b++)) |
| return -1; |
| } |
| return 0; |
| } |
| |
| int |
| cistrcmp(char *a, char *b) |
| { |
| while(*a || *b) |
| if(tolower((uchar)*a++) != tolower((uchar)*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; |
| } |
| |