/* * Anonymous-only FTP daemon. * * Designed to be run from inetd; as such, we speak protocol on stdin * and stdout and assume they are the already-set-up control network * connection. We also assume stdin and stdout are connected to the * same place, and, in particular, that stdin is appropriate for calls * such as getsockname() and getpeername(). * * Command-line consists of flags, some of which take args: * * -myname NAME * Name to present as. Defaults to sysctl kern.hostname. * * -stderr * Log to stderr. Default is to log to syslog with * facility LOG_FTP. * * FTP protocol references: * * RFC 0959 * The basic spec. * RFC 1123 * Response to PASV * RFC 1639 * FOOBAR: LPRT, LPSV, Some new response codes * RFC 2228 * Security extensions (irrelevant to us). * RFC 2389 * FEAT and OPTS * RFC 2428 * EPRT and EPSV (including 522 reply) * RFC 2640 * I18n of the command channel and filenames. * RFC 2773 * Encryption with KEA/SKIPJACK (irrelevant to us). * RFC 3659 * Various extensions: MDTM, SIZE, REST, TVFS, MLST, MLSD. * RFC 5797 * Registry of FTP commands and extensions * RFC 7151 * HOST command for vhost support (irrelevant to us). * * Many of these are maddeningly incomplete. For example, 3659 does * not specify what to do if an error occurs partway through reading * and sending directory entries for MLSD. * * We support full 8-bit pathnames, including NUL-stuffing of CRs, in * both directions. Because our filenames are octet strings, not * character strings, we follow the last paragraph of 3.1 and treat * them as raw octet sequences, except for the aforementioned * NUL-stuffing of CRs. * * I'm somewhat on the fence about including UTF8 in a FEAT response. * We do not actually support UTF-8 in any meaningful sense; what we * do do is support full 8-bit pathnames. But that's all 2640 * specifies we have to to return UTF8; it's just a misleading name. * And 3659 specifies that "[a]ll implementations supporting MLST MUST * support [7]", [7] being 2640. * * We do not support LANG in any useful sense. The only language we * support is en. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern const char *__progname; #include "stdio-util.h" static const char *myname = 0; static int log_stderr = 0; typedef enum { DCS_NONE = 1, DCS_PORT, DCS_PASV, DCS_EPRT, DCS_EPSV, DCS_LIVE, } DATACONN_STATE; typedef enum { XT_ASCII = 1, // implicitly non-print XT_IMAGE, XT_LOCAL_8, // FTP defines more; we don't support others. } XFER_TYPE; typedef unsigned long long int ULLI; typedef struct hostinfo HOSTINFO; typedef struct telnet_state TELNET_STATE; typedef struct cmd CMD; typedef struct epxx_args EPXX_ARGS; typedef struct mlsx_fact MLSX_FACT; struct mlsx_fact { const char *name; unsigned int bit; void (*gen_mlsx_feat)(ES *, const FSTREE_STAT *); int namelen; } ; struct epxx_args { const char *err; int af0; int afl; int addr0; int addrl; int port0; int portl; } ; struct cmd { const char *name; int namelen; void (*impl)(const unsigned char *, int, const unsigned char *, int); #define IMPL_ARGS\ const unsigned char *cmd __attribute__((__unused__)), \ int cmdl __attribute__((__unused__)), \ const unsigned char *arg __attribute__((__unused__)), \ int argl __attribute__((__unused__)) } ; #define CMD_NORMAL(name) { #name, sizeof(#name)-1, cmd_impl_##name } #define CMD_UNIMP(name) { #name, sizeof(#name)-1, cmd_impl_unimp } #define CMD_WRITE(name) { #name, sizeof(#name)-1, cmd_impl_write } struct telnet_state { unsigned char flags; #define TSF_IAC 0x01 // just after IAC #define TSF_SB 0x02 // in IAC SB ... IAC SE #define TSF_OPT 0x0c // just after IAC xyz, where xyz is... #define TSF_OPT_NONE 0x00 // none #define TSF_OPT_DO 0x04 // ... DO #define TSF_OPT_WILL 0x08 // ... WILL #define TSF_OPT_xONT 0x0c // ... DONT or WONT } ; struct hostinfo { struct sockaddr *sa; socklen_t len; char *txt; } ; static int nullfd; static HOSTINFO my_hi; static HOSTINFO peer_hi; static AIO_OQ out_oq; static TELNET_STATE cmdtns; static ES cmdline; static int in_id; static int out_id; static int done_id; static int login_step; static int logged_in; static FSTREE *fstree; static FSTREE_OF *cwd; static DATACONN_STATE dataconn_state; static XFER_TYPE type = XT_ASCII; #define FACT_SIZE 0x00000001 #define FACT_MODIFY 0x00000002 #define FACT_TYPE 0x00000004 static unsigned int mlsx_mask = FACT_SIZE | FACT_MODIFY | FACT_TYPE; static MLSX_FACT mlsx_facts[]; // forward static const int mlsx_nfacts; // forward static struct sockaddr_storage psa; static int psalen; static int dataconn; static HOSTINFO my_di; static HOSTINFO peer_di; static FSTREE_OF *tosend; static AIO_OQ sendq; static int send_rid; static int send_wid; static int epsv_all; static time_t last_activity; static int idle_id; static int idle_timeout = 600; #define Cisspace(x) isspace((unsigned char)(x)) #define Cisdigit(x) isdigit((unsigned char)(x)) static void *dequal(const volatile void *a) { return(*(void **)(void *)&a); } static void nbio(int fd) { fcntl(fd,F_SETFL,fcntl(fd,F_GETFL,0)|O_NONBLOCK); } static void handleargs(int ac, char **av) { int skip; int errs; skip = 0; errs = 0; for (ac--,av++;ac;ac--,av++) { if (skip > 0) { skip --; continue; } if (**av != '-') { fprintf(stderr,"%s: stray argument `%s'\n",__progname,*av); errs = 1; } if (0) { needarg:; fprintf(stderr,"%s: %s needs a following argument\n",__progname,*av); errs = 1; continue; } #define WANTARG() do { if (++skip >= ac) goto needarg; } while (0) if (!strcmp(*av,"-myname")) { WANTARG(); myname = av[skip]; continue; } if (!strcmp(*av,"-stderr")) { log_stderr = 1; continue; } #undef WANTARG fprintf(stderr,"%s: unrecognized option `%s'\n",__progname,*av); errs = 1; } if (errs) exit(1); } static void vlogmsg(const char *pref, int sev, const char *fmt, va_list ap, int syslogpref) { char *msg; int len; len = vasprintf(&msg,fmt,ap); if (log_stderr) { struct iovec iov[4]; iov[0].iov_base = dequal(pref); iov[0].iov_len = strlen(pref); iov[1].iov_base = dequal(": "); iov[1].iov_len = 2; iov[2].iov_base = msg; iov[2].iov_len = len; iov[3].iov_base = dequal("\n"); iov[3].iov_len = 1; writev(2,&iov[0],4); } else { syslog(sev,"%s%s%s\n",syslogpref?pref:"",syslogpref?": ":"",msg); } } static void quietdrop(void) { //#ifdef TCP_QUIETDROP int on; on = 1; setsockopt(0,IPPROTO_TCP,TCP_QUIETDROP,&on,sizeof(on)); sleep(5); exit(0); //#endif } static void fatal(const char *, ...) __attribute__((__format__(__printf__,1,2),__noreturn__)); static void fatal(const char *fmt, ...) { va_list ap; va_start(ap,fmt); vlogmsg("fatal",LOG_CRIT,fmt,ap,1); va_end(ap); exit(1); } static void error(const char *, ...) __attribute__((__format__(__printf__,1,2))); static void error(const char *fmt, ...) { va_list ap; va_start(ap,fmt); vlogmsg("error",LOG_ERR,fmt,ap,0); va_end(ap); } static void warning(const char *, ...) __attribute__((__format__(__printf__,1,2))); static void warning(const char *fmt, ...) { va_list ap; va_start(ap,fmt); vlogmsg("warning",LOG_WARNING,fmt,ap,0); va_end(ap); } static void notice(const char *, ...) __attribute__((__format__(__printf__,1,2))); static void notice(const char *fmt, ...) { va_list ap; va_start(ap,fmt); vlogmsg("notice",LOG_NOTICE,fmt,ap,0); va_end(ap); } static void info(const char *, ...) __attribute__((__format__(__printf__,1,2))); static void info(const char *fmt, ...) { va_list ap; va_start(ap,fmt); vlogmsg("info",LOG_INFO,fmt,ap,0); va_end(ap); } /* * gethostname(), the simple interface for getting the hostname, is * badly designed; it provides no way to tell how long the hostname is * without actually fetching it. This makes it impossible to * preallocate a buffer for the name (because we can't tell how long * to make it). So we fetch sysctl kern.hostname instead, which * doesn't have the analogous design bug. */ static void setup_defaults(void) { if (! myname) { int mib[2]; size_t len; char *buf; size_t len2; mib[0] = CTL_KERN; mib[1] = KERN_HOSTNAME; if (sysctl(&mib[0],2,0,&len,0,0) < 0) fatal("can't get kern.hostname (1): %s",strerror(errno)); len2 = len + 1; buf = malloc(len2); if (sysctl(&mib[0],2,buf,&len2,0,0) < 0) fatal("can't get kern.hostname (2): %s",strerror(errno)); if (len2 > len) fatal("can't get kern.hostname: %d > %d",len2,len); buf[len2] = '\0'; myname = buf; } } static void open_stdio(void) { int fd; fd = open("/dev/null",O_RDWR,0); if (fd < 0) fatal("can't open /dev/null: %s",strerror(errno)); while (fd < 3) { fd = dup(fd); if (fd < 0) fatal("can't dup /dev/null: %s",strerror(errno)); } nullfd = fd; } static void get_addr(int fd, HOSTINFO *hi, int (*getfn)(int, struct sockaddr *, socklen_t *), const char *fnname, const char *what) { struct sockaddr_storage ss; socklen_t sslen; char hbuf[NI_MAXHOST]; char sbuf[NI_MAXSERV]; int e; void *sa; char *txt; sslen = sizeof(ss); if ((*getfn)(fd,(void *)&ss,&sslen) < 0) fatal("%s on %s: %s",fnname,strerror(errno),what); e = getnameinfo((void *)&ss,sslen,&hbuf[0],sizeof(hbuf),&sbuf[0],sizeof(sbuf),NI_NUMERICHOST|NI_NUMERICSERV); if (e) fatal("%s -> getnameinfo: %s",fnname,gai_strerror(e)); if (asprintf(&txt,"%s/%s",&hbuf[0],&sbuf[0]) < 0) fatal("%s -> getnameinfo -> asprintf failed\n",fnname); sa = malloc(sslen); bcopy(&ss,sa,sslen); hi->sa = sa; hi->len = sslen; hi->txt = txt; } static void get_ctl_addrs(void) { get_addr(0,&my_hi,&getsockname,"getsockname","stdin"); get_addr(0,&peer_hi,&getpeername,"getpeername","stdin"); } static void get_data_addrs(void) { get_addr(dataconn,&my_di,&getsockname,"getsockname","data connection"); get_addr(dataconn,&peer_di,&getpeername,"getpeername","data connection"); } static void telnet_init(TELNET_STATE *s) { s->flags = TSF_OPT_NONE; } /* * The TELNET spec is remarkably ill-specified. For example, what (if * anything) does IAC WILL IAC SE mean? What about IAC DO IAC WONT? */ static int telnet_input(TELNET_STATE *s, unsigned char c) { unsigned char oresp[3]; if (s->flags & TSF_IAC) { s->flags &= ~TSF_IAC; switch (c) { case 240: // SE s->flags &= ~TSF_SB; break; case 241: // NOP case 242: // DM case 243: // BREAK case 244: // IP case 245: // AO case 246: // AYT case 247: // EC case 248: // EL case 249: // GA // ignore break; case 250: // SB s->flags |= TSF_SB; break; case 251: // WILL s->flags = (s->flags & ~TSF_OPT) | TSF_OPT_WILL; break; case 252: // WONT s->flags = (s->flags & ~TSF_OPT) | TSF_OPT_xONT; break; case 253: // DO s->flags = (s->flags & ~TSF_OPT) | TSF_OPT_DO; break; case 254: // DONT s->flags = (s->flags & ~TSF_OPT) | TSF_OPT_xONT; break; default: // includes doubled IAC return(c); break; } return(-1); } else { if (c == 255) { s->flags |= TSF_IAC; return(-1); } switch (s->flags & TSF_OPT) { case TSF_OPT_NONE: if (s->flags & TSF_SB) return(-1); return(c); break; case TSF_OPT_WILL: { oresp[0] = 255; // IAC oresp[1] = 254; // DONT oresp[2] = c; aio_oq_queue_copy(&out_oq,&oresp[0],3); } break; case TSF_OPT_DO: { oresp[0] = 255; // IAC oresp[1] = 252; // WONT oresp[2] = c; aio_oq_queue_copy(&out_oq,&oresp[0],3); } break; case TSF_OPT_xONT: break; default: fatal("impossible telnet state flags %02x\n",s->flags); break; } s->flags = (s->flags & ~TSF_OPT) | TSF_OPT_NONE; return(-1); } } static int std_out(AIO_OQ *oq, int fd) { int nw; nw = aio_oq_writev(oq,fd,-1); if (nw < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return(0); break; } return(-1); } aio_oq_dropdata(oq,nw); return(0); } /* * This really should be driven off a config file. * * It also should be kept in sync with bozohtpd, preferably shared. */ static void fs_init(void) { void *p; int e; p = fstree_unix_priv("/",&e); if (! p) fatal("fstree_unix_priv /: %s",fstree_strerror(e?e:FE_INTERNAL)); fstree = fstree_init(&fstree_unix_ops,FSTREE_UNIX_ROOT,p,&e); if (! fstree) fatal("fstree_init: %s",fstree_strerror(e?e:FE_INTERNAL)); e = fstree_mount(fstree,fstree_s_nul2c("/admin"),&fstree_empty_ops,FSTREE_EMPTY_ROOT,0); if (e) fatal("fstree_mount /admin: %s",fstree_strerror(e)); e = fstree_mount(fstree,fstree_s_nul2c("/local"),&fstree_empty_ops,FSTREE_EMPTY_ROOT,0); if (e) fatal("fstree_mount /local: %s",fstree_strerror(e)); e = fstree_mount(fstree,fstree_s_nul2c("/usr"),&fstree_empty_ops,FSTREE_EMPTY_ROOT,0); if (e) fatal("fstree_mount /usr: %s",fstree_strerror(e)); // qfs failure is nonfatal p = fstree_qfs_priv("/admin/qfs/gits",&e); if (p) fstree_mount(fstree,fstree_s_nul2c("/pub/mouse/git-unpacked"),&fstree_qfs_ops,FSTREE_QFS_ROOT,p); cwd = fstree_open_root(fstree); } static void walk_parents(ES *es, FSTREE_OF *d, void (*outerfail)(int)) { __label__ failto; FSTREE_OF *p; int err; FSTREE_DIRENT ents[64]; char estrs[1024]; int rv; long int o; int i; FSTREE_OF *c; int j; int n; unsigned char ch; void fail(int e) { err = e; goto failto; } if (0) { failto:; if (c) fstree_close(c); if (p) fstree_close(p); (*outerfail)(err); } c = 0; p = fstree_open(d,fstree_s_pl2c("..",2),&err); if (! p) fail(err); if (fstree_unique(p) == fstree_unique(d)) { fstree_close(p); return; } rv = fstree_readdir(p,&o,0,0,0,0); if (rv < 0) fail(-rv); if (rv > 0) fail(FE_INTERNAL); while (1) { rv = fstree_readdir(p,&o,&ents[0],sizeof(ents)/sizeof(ents[0]),&estrs[0],sizeof(estrs)); if (rv == 0) break; if (rv < 0) fail(-rv); for (i=0;i= 0) { close(dataconn); dataconn = -1; } free(my_di.txt); my_di.txt = 0; free(peer_di.txt); peer_di.txt = 0; if (tosend) { fstree_close(tosend); tosend = 0; } aio_oq_flush(&sendq); if (send_rid != AIO_NOID) { aio_remove_block(send_rid); send_rid = AIO_NOID; } if (send_wid != AIO_NOID) { aio_remove_poll(send_wid); send_wid = AIO_NOID; } dataconn_state = DCS_NONE; } static int perform_abort(void) { switch (dataconn_state) { case DCS_NONE: // Nothing to do return(0); break; case DCS_PORT: case DCS_PASV: case DCS_EPRT: case DCS_EPSV: case DCS_LIVE: abort_send(); return(1); break; default: fatal("impossible data connection state %d",(int)dataconn_state); break; } } static int check_done(void *arg __attribute__((__unused__))) { if ((in_id == AIO_NOID) && aio_oq_empty(&out_oq)) { info("exiting: input closed, output drained"); exit(0); } return(AIO_BLOCK_NIL); } static void close_input(void) { aio_remove_poll(in_id); in_id = AIO_NOID; done_id = aio_add_block(&check_done,0); } static int ascii_size(FSTREE_CSTRING fn) { FSTREE_OF *of; char rbuf[8192]; int nr; int i; int rv; int err; of = fstree_open(cwd,fn,&err); if (! of) return(-err); rv = 0; while (1) { nr = fstree_read(of,&rbuf[0],sizeof(rbuf)); if (nr < 0) { fstree_close(of); return(nr); } if (nr == 0) { fstree_close(of); return(rv); } for (i=0;i= 0) { aio_oq_queue_point(q,dp+d0,i-d0); d0 = -1; } } while (0) #define BEGIN() do { if (d0 < 0) d0 = i; } while (0) for (i=0;i=0;i--) { if (mlsx_mask & mlsx_facts[i].bit) (*mlsx_facts[i].gen_mlsx_feat)(e,stb); } es_append_1(e,' '); es_append_n(e,s->s,s->l); } static void gen_plain(ES *s, const FSTREE_STAT *stb, FSTREE_CSTRING n) { time_t tt; struct tm *tm; tt = stb->u.plain.mtime; tm = gmtime(&tt); es_append_printf(s,"%-39.*s %04d-%02d-%02d %02d:%02d:%02d %12llu", n.l, n.s, tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, (ULLI)stb->u.plain.size ); } static void gen_link(ES *s, FSTREE_OF *p, FSTREE_CSTRING n) { char *b; int l; int r; l = fstree_readlink(p,n,0,0); if (l < 0) { es_append_printf(s,"%-39.*s [%s]",n.l,n.s,fstree_strerror(-l)); } else { b = malloc(l+1); r = fstree_readlink(p,n,b,l); b[l] = '\0'; es_append_printf(s,"%-39.*s -> %s",n.l,n.s,b); free(b); } } static void gen_dir(ES *s, FSTREE_CSTRING n) { es_append_printf(s,"%.*s/",n.l,n.s); } static int contains_crlf(const unsigned char *s, int l) { int i; for (i=1;i 999)) abort(); info("<--- %03d %.*s",code,l,s); aio_oq_queue_printf(&out_oq,"%03d ",code); queue_escaped_reply(&out_oq,s,l); aio_oq_queue_point(&out_oq,"\r\n",2); } static void reply_first(int, const char *, ...) __attribute__((__format__(__printf__,2,3))); static void reply_first(int code, const char *fmt, ...) { va_list ap; char *s; int l; va_start(ap,fmt); l = vasprintf(&s,fmt,ap); va_end(ap); if ((code < 0) || (code > 999)) abort(); info("<--- %03d-%.*s",code,l,s); aio_oq_queue_printf(&out_oq,"%03d-",code); queue_escaped_reply(&out_oq,s,l); aio_oq_queue_point(&out_oq,"\r\n",2); } static void reply_mid(const char *, ...) __attribute__((__format__(__printf__,1,2))); static void reply_mid(const char *fmt, ...) { va_list ap; char *s; int l; va_start(ap,fmt); l = vasprintf(&s,fmt,ap); va_end(ap); info("<--- %.*s",l,s); aio_oq_queue_point(&out_oq," ",1); queue_escaped_reply(&out_oq,s,l); aio_oq_queue_point(&out_oq,"\r\n",2); } static void reply_last(int, const char *, ...) __attribute__((__format__(__printf__,2,3))); static void reply_last(int code, const char *fmt, ...) { va_list ap; char *s; int l; va_start(ap,fmt); l = vasprintf(&s,fmt,ap); va_end(ap); if ((code < 0) || (code > 999)) abort(); info("<--- %03d %.*s",code,l,s); aio_oq_queue_printf(&out_oq,"%03d ",code); queue_escaped_reply(&out_oq,s,l); aio_oq_queue_point(&out_oq,"\r\n",2); } static void activity(void) { last_activity = time(0); } /* * Strictly, this violates 959, in that we use a non-default data port * even for server-initiated connections. However, no known FTP * clients care, and it would require us to run with privilege to * "fix" this "problem". Until and unless this is demonstrably an * operational problem, I'm going to consider that aspect an * early-Internet legacy spec that's no longer relevant. */ static int get_data_conn(void) { int new; struct sockaddr_storage from; socklen_t fromlen; switch (dataconn_state) { case DCS_NONE: reply(550,"Use of default data ports not supported"); return(1); break; case DCS_PORT: case DCS_EPRT: dataconn = socket(my_hi.sa->sa_family,SOCK_STREAM,0); if (dataconn < 0) { reply(425,"Can't create endpoint: %s",strerror(errno)); return(1); } if (connect(dataconn,(const void *)&psa,sizeof(struct sockaddr_in)) < 0) { reply(425,"Connection failed: %s",strerror(errno)); close(dataconn); dataconn = -1; return(1); } get_data_addrs(); dataconn_state = DCS_LIVE; return(0); break; case DCS_PASV: case DCS_EPSV: fromlen = sizeof(from); new = accept(dataconn,(void *)&from,&fromlen); if (new < 0) { reply(425,"Can't accept connection: %s",strerror(errno)); return(1); } // XXX Maybe check from.sin_addr == my_hi.sa->sin_addr? close(dataconn); dataconn = new; get_data_addrs(); dataconn_state = DCS_LIVE; return(0); break; default: abort(); break; } } static void done_send(AIO_SPECIAL_OP op, void *a __attribute__((__unused__)), int b __attribute__((__unused__))) { if (op != AIO_SPECIAL_NORMAL) return; abort_send(); reply(226,"Done"); } static void queue_ascii(AIO_OQ *q, const char *data, int len) { int o; const char *nl; int nlo; o = 0; while (o < len) { nl = memchr(data+o,'\n',len-o); if (! nl) { aio_oq_queue_copy(q,data+o,len-o); return; } nlo = nl - data; if (nlo > o) aio_oq_queue_copy(q,data+o,nlo-o); aio_oq_queue_point(q,"\r\n",2); o = nlo + 1; } } static int read_send_data(void *arg __attribute__((__unused__))) { char rbuf[8192]; int nr; if (aio_oq_qlen(&sendq) > 524288) return(AIO_BLOCK_NIL); while (aio_oq_qlen(&sendq) < 1048576) { nr = fstree_read(tosend,&rbuf[0],sizeof(rbuf)); if (nr < 0) { abort_send(); reply(451,"Read error: %s",fstree_strerror(-nr)); return(AIO_BLOCK_LOOP); } if (nr == 0) { aio_remove_block(send_rid); send_rid = AIO_NOID; aio_oq_queue_special(&sendq,AIO_SPECIAL_WRITE,&done_send,0,0); return(AIO_BLOCK_LOOP); } switch (type) { case XT_ASCII: queue_ascii(&sendq,&rbuf[0],nr); break; case XT_IMAGE: case XT_LOCAL_8: aio_oq_queue_copy(&sendq,&rbuf[0],nr); break; } } return(AIO_BLOCK_LOOP); } static int wtest_data(void *arg __attribute__((__unused__))) { return(aio_oq_nonempty(&sendq)); } static void w_data(void *arg __attribute__((__unused__))) { int nw; nw = aio_oq_writev(&sendq,dataconn,-1); if (nw < 0) { switch (nw) { case AIO_WRITEV_SPECIAL: // Special callback did the work return; break; case AIO_WRITEV_ERROR: abort_send(); reply(451,"Data connection write error: %s",strerror(errno)); return; break; default: // aio_oq_writev broken, or we need updating fatal("unexpected return %d from aio_oq_writev",nw); break; } } activity(); aio_oq_dropdata(&sendq,nw); } static void do_listing(const char *cmd, int showinfo, const char *arg, int argl) { FSTREE_CSTRING as; FSTREE_OF *d; FSTREE_STAT stb; int err; static ES s = ES_STATIC_INIT; long int o; int n; int i; FSTREE_DIRENT e; FSTREE_CSTRING en; char nbuf[1024]; const char *eol; int eoll; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } as = (argl < 1) ? fstree_s_pl2c(".",1) : fstree_s_pl2c(arg,argl); switch (type) { case XT_ASCII: eol = "\r\n"; eoll = 2; break; case XT_IMAGE: case XT_LOCAL_8: eol = "\n"; eoll = 1; break; default: abort(); break; } stb = fstree_stat(cwd,as,FSTREE_STAT_NOFOLLOW); if (!showinfo && (stb.type != FST_DIR)) { reply(501,"LCan't %s: %s",cmd,fstree_strerror(FE_NOTDIR)); return; } es_clear(&s); switch (stb.type) { case FST_ERR: fstree_close(d); reply(501,"Can't %s: %s",cmd,fstree_strerror(stb.u.err.err)); break; case FST_PLAIN: gen_plain(&s,&stb,as); break; case FST_LINK: gen_link(&s,cwd,as); break; case FST_DIR: d = fstree_open(cwd,as,&err); if (! d) { reply(501,"Can't %s: %s",cmd,fstree_strerror(err)); return; } n = fstree_readdir(d,&o,0,0,0,0); if (n < 0) { fstree_close(d); reply(550,"Internal error"); error("MLSD: %s",fstree_strerror(-n)); return; } if (n > 0) { fstree_close(d); reply(550,"Internal error"); error("MLSD: first readdir returned %d",n); return; } while <"dir"> (1) { n = fstree_readdir(d,&o,&e,1,&nbuf[0],sizeof(nbuf)); if (n < 0) { reply(550,"Internal error"); error("MSLD: readdir: %s",fstree_strerror(-n)); es_done(&s); fstree_close(d); return; } if (n == 0) break; if (contains_crlf(e.name,e.namelen)) continue <"dir">; en = fstree_s_pl2c(e.name,e.namelen); if (showinfo) { stb = fstree_stat(d,en,FSTREE_STAT_NOFOLLOW); switch (stb.type) { default: continue <"dir">; break; case FST_PLAIN: gen_plain(&s,&stb,en); break; case FST_LINK: gen_link(&s,d,en); break; case FST_DIR: gen_dir(&s,en); break; } } else { es_append_n(&s,en.s,en.l); } es_append_n(&s,eol,eoll); } fstree_close(d); break; } if (get_data_conn()) { es_done(&s); return; } reply(150,"OK"); i = es_len(&s); tosend = 0; aio_oq_flush(&sendq); aio_oq_queue_free(&sendq,es_take(&s),i); aio_oq_queue_special(&sendq,AIO_SPECIAL_WRITE,&done_send,0,0); send_rid = AIO_NOID; send_wid = aio_add_poll(dataconn,&aio_rwtest_never,&wtest_data,0,&w_data,0); } static void new_command(void) { es_clear(&cmdline); } static EPXX_ARGS crack_epxx_args(const unsigned char *arg, int argl) { EPXX_ARGS a; unsigned char delim; int i; if (argl < 1) return((EPXX_ARGS){.err="EPRT requires an argument"}); delim = arg[0]; if ((delim < 33) || (delim > 126)) return((EPXX_ARGS){.err="Invalid EPRT delimiter"}); do <"badargs"> { a.af0 = 1; for (i=1;(i= argl) break <"badargs">; a.afl = i - a.af0; a.addr0 = ++i; for (;(i= argl) break <"badargs">; a.addrl = i - a.addr0; a.port0 = ++i; for (;(i= argl) break <"badargs">; a.portl = i - a.port0; if (argl != i+1) return((EPXX_ARGS){.err="Junk after argument"}); a.err = 0; return(a); } while (0); return((EPXX_ARGS){.err="Error parsing argument"}); } static int same_host(const struct sockaddr *a, const struct sockaddr *b) { if (a->sa_family != b->sa_family) return(0); switch (a->sa_family) { default: return(0); break; case AF_INET: return( ((const struct sockaddr_in *)a)->sin_addr.s_addr == ((const struct sockaddr_in *)b)->sin_addr.s_addr ); break; case AF_INET6: return(!bcmp( &((const struct sockaddr_in6 *)a)->sin6_addr, &((const struct sockaddr_in6 *)b)->sin6_addr, 16 )); break; } } /* * Get /admin/motd from the underlying filesystem, not the fstree! */ static void anon_login_reply(void) { FILE *f; ES e; int c; f = fopen("/admin/motd","r"); if (! f) { reply(230,"Guest access OK"); return; } reply_first(230,""); es_init(&e); while (1) { c = getc(f); if (c == EOF) break; if (c == '\n') { reply_mid("%.*s",es_len(&e),es_buf(&e)); es_clear(&e); continue; } es_append_1(&e,c); } if (es_len(&e)) reply_mid("%.*s",es_len(&e),es_buf(&e)); reply_last(230,"Guest login OK"); es_done(&e); fclose(f); } static void ensure_fact_lengths(void) { int i; if (mlsx_facts[0].namelen) return; for (i=mlsx_nfacts-1;i>=0;i--) mlsx_facts[i].namelen = strlen(mlsx_facts[i].name); } static void gen_fact_feat(ES *e, int x) { es_append_n(e,mlsx_facts[x].name,mlsx_facts[x].namelen); if (mlsx_mask & mlsx_facts[x].bit) es_append_1(e,'*'); es_append_1(e,';'); } static void gen_fact_opts(ES *e, int x) { if (mlsx_mask & mlsx_facts[x].bit) es_append_n(e,mlsx_facts[x].name,mlsx_facts[x].namelen); es_append_1(e,';'); } static void impl_cd_common(const char *path, int len) { FSTREE_OF *new; int e; FSTREE_STAT stb; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } new = fstree_open(cwd,fstree_s_pl2c(path,len),&e); if (! new) { reply(550,"%s",fstree_strerror(e)); return; } stb = fstree_fstat(new); if (stb.type == FST_ERR) { fstree_close(new); reply(550,"%s",fstree_strerror(stb.u.err.err)); return; } if (stb.type != FST_DIR) { fstree_close(new); reply(550,"%s",fstree_strerror(FE_NOTDIR)); return; } fstree_close(cwd); cwd = new; reply(250,"OK"); } static void cmd_impl_unknown(IMPL_ARGS) { reply(500,"`%.*s' unrecognized",cmdl,cmd); } static void cmd_impl_unimp(IMPL_ARGS) { reply(502,"`%.*s' not implemented",cmdl,cmd); } static void cmd_impl_write(IMPL_ARGS) { warning("Write attempt from %s\n",peer_hi.txt); quietdrop(); reply(550,"No privilege"); } static void cmd_impl_CWD(IMPL_ARGS) { impl_cd_common(arg,argl); } static void cmd_impl_PWD(IMPL_ARGS) { ES cwd; int e; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } es_init(&cwd); e = fs_get_cwd(&cwd); if (e) { reply(550,"Can't get current working directory (%s)",fstree_strerror(e)); } else { reply(257,"\"%.*s\"",es_len(&cwd),es_buf(&cwd)); } es_done(&cwd); } static void cmd_impl_ABOR(IMPL_ARGS) { if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if (perform_abort()) reply(426,"Aborted"); reply(226,"OK"); } static void cmd_impl_CDUP(IMPL_ARGS) { impl_cd_common("..",2); } static void cmd_impl_EPRT(IMPL_ARGS) { EPXX_ARGS parsed; int i; static ES es = ES_STATIC_INIT; int v; int n1; int n2; struct addrinfo hints; struct addrinfo *ai; char *hoststr; char *portstr; const char *afstr; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if (epsv_all) { reply(550,"EPSV ALL used"); return; } parsed = crack_epxx_args(arg,argl); if (parsed.err) { reply(501,"%s",parsed.err); return; } es_clear(&es); es_append_n(&es,arg+parsed.af0,parsed.afl); es_append_1(&es,'\0'); n1 = -1; n2 = -1; sscanf(es_buf(&es),"%d %n%*c%n",&v,&n1,&n2); if ((n1 < 0) || (n2 >= 0)) { reply(501,"Invalid address family"); return; } switch (v) { case 1: i = AF_INET; afstr = "IPv4"; break; case 2: i = AF_INET6; afstr = "IPv6"; break; default: reply(522,"Unsupported address family; use (1,2)"); return; break; } if (i != peer_hi.sa->sa_family) { reply(550,"EPRT must specify same address family as control connection"); return; } hoststr = malloc(parsed.addrl+1); bcopy(arg+parsed.addr0,hoststr,parsed.addrl); hoststr[parsed.addrl] = '\0'; portstr = malloc(parsed.portl+1); bcopy(arg+parsed.port0,portstr,parsed.portl); portstr[parsed.portl] = '\0'; hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV; hints.ai_family = peer_hi.sa->sa_family; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; hints.ai_addrlen = 0; // XXX API botch hints.ai_canonname = 0; // XXX API botch hints.ai_addr = 0; // XXX API botch hints.ai_next = 0; // XXX API botch ai = 0; i = getaddrinfo(hoststr,portstr,&hints,&ai); do <"fail"> { if (i) { reply(501,"Can't parse %s %s/%s [%s]",afstr,hoststr,portstr,gai_strerror(i)); break <"fail">; } if (! ai) { reply(550,"Internal error"); error("EPRT %s %s/%s: lookup succeeded with no addresses",afstr,hoststr,portstr); break <"fail">; } if (ai->ai_next) { reply(550,"Internal error"); error("EPRT %s %s/%s: lookup succeeded with multiple addresses",afstr,hoststr,portstr); break <"fail">; } if (ai->ai_addr->sa_family != hints.ai_family) { reply(550,"Internal error"); error("EPRT %s %s/%s: lookup got AF %d, expected %d",afstr,hoststr,portstr,ai->ai_addr->sa_family,hints.ai_family); break <"fail">; } if (ai->ai_addrlen > sizeof(struct sockaddr_storage)) { reply(550,"Internal error"); error("EPRT %s %s/%s: lookup addr size %d, max %d",afstr,hoststr,portstr,ai->ai_addrlen,(int)sizeof(struct sockaddr_storage)); break <"fail">; } if (! same_host(peer_hi.sa,ai->ai_addr)) { reply(550,"Address must be client address"); break <"fail">; } switch (dataconn_state) { case DCS_NONE: break; case DCS_PORT: case DCS_PASV: case DCS_EPRT: case DCS_EPSV: abort_send(); break; case DCS_LIVE: reply(550,"Data transfer already in progress"); return; break; default: fatal("impossible data connection state %d",(int)dataconn_state); break; } bcopy(ai->ai_addr,&psa,ai->ai_addrlen); psalen = ai->ai_addrlen; dataconn_state = DCS_EPRT; reply(200,"OK"); } while (0); if (ai) freeaddrinfo(ai); free(hoststr); free(portstr); } static void cmd_impl_EPSV(IMPL_ARGS) { struct sockaddr_storage ss; int p; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if ((argl == 3) && !strncasecmp(arg,"all",3)) { epsv_all = 1; reply(200,"OK"); return; } if (argl >= 0) { reply(550,"Third-party transfers not supported"); return; } switch (dataconn_state) { case DCS_NONE: break; case DCS_PORT: case DCS_PASV: case DCS_EPRT: case DCS_EPSV: abort_send(); break; case DCS_LIVE: reply(550,"Data transfer already in progress"); return; break; default: fatal("impossible data connection state %d",(int)dataconn_state); break; } dataconn = socket(my_hi.sa->sa_family,SOCK_STREAM,0); if (dataconn < 0) { reply(550,"Internal error"); error("EPSV: socket (af %d): %s",my_hi.sa->sa_family,strerror(errno)); return; } bzero(&ss,sizeof(ss)); // XXX API botch switch (my_hi.sa->sa_family) { case AF_INET: ((struct sockaddr_in *)&ss)->sin_family = AF_INET; ((struct sockaddr_in *)&ss)->sin_len = sizeof(struct sockaddr_in); ((struct sockaddr_in *)&ss)->sin_addr = ((struct sockaddr_in *)my_hi.sa)->sin_addr; break; case AF_INET6: ((struct sockaddr_in6 *)&ss)->sin6_family = AF_INET6; ((struct sockaddr_in6 *)&ss)->sin6_len = sizeof(struct sockaddr_in6); bcopy(&((struct sockaddr_in6 *)my_hi.sa)->sin6_addr,&((struct sockaddr_in6 *)&ss)->sin6_addr,16); break; default: fatal("EPSV: bad AF %d",my_hi.sa->sa_family); break; } if (bind(dataconn,(const void *)&ss,ss.ss_len) < 0) { reply(550,"Internal error"); error("EPSV: bind: %s",strerror(errno)); close(dataconn); dataconn = -1; return; } if (listen(dataconn,5) < 0) { reply(550,"Internal error"); error("EPSV: listen: %s",strerror(errno)); close(dataconn); dataconn = -1; return; } get_addr(dataconn,&my_di,&getsockname,"getsockname","listening port"); switch (my_hi.sa->sa_family) { case AF_INET: p = ntohs(((const struct sockaddr_in *)my_di.sa)->sin_port); break; case AF_INET6: p = ntohs(((const struct sockaddr_in6 *)my_di.sa)->sin6_port); break; default: fatal("EPSV: bad AF %d",my_hi.sa->sa_family); break; } reply(229,"EPSV: (|||%d|)",p); dataconn_state = DCS_EPSV; } static void cmd_impl_FEAT(IMPL_ARGS) { ES e; int i; if (argl > 0) { reply(501,"Takes no arguments"); return; } reply_first(211,"Features:"); reply_mid("MDTM"); es_init(&e); ensure_fact_lengths(); for (i=mlsx_nfacts-1;i>=0;i--) gen_fact_feat(&e,i); reply_mid("MLST %.*s",es_len(&e),es_buf(&e)); es_done(&e); reply_mid("SIZE"); reply_mid("LANG EN*"); reply_mid("UTF8"); // See file comment header reply_last(211,"End"); } static void cmd_impl_HELP(IMPL_ARGS) { reply(211,"Not implemented"); } static void cmd_impl_LANG(IMPL_ARGS) { int i; if (argl < 1) { reply(200,"Language set to default English"); return; } for (i=0;(i 8) { reply(501,"Language tag too long"); return; } if ((i != 2) || strncasecmp(arg,"en",2)) { reply(504,"Language `%.*s' not supported, using EN",i,arg); } else { reply(200,"Language set to English"); } } static void cmd_impl_LIST(IMPL_ARGS) { do_listing("LIST",1,arg,argl); } static void cmd_impl_MDTM(IMPL_ARGS) { FSTREE_STAT stb; time_t tt; struct tm *tm; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if (argl < 1) { reply(501,"Need an argument"); return; } stb = fstree_stat(cwd,fstree_s_pl2c(arg,argl),0); switch (stb.type) { case FST_ERR: reply(550,"No MDTM available: %s",fstree_strerror(stb.u.err.err)); break; case FST_PLAIN: tt = stb.u.plain.mtime; tm = gmtime(&tt); reply(213,"%04u%02u%02u%02u%02u%02u",tm->tm_year+1900,tm->tm_mon+1,tm->tm_mday,tm->tm_hour,tm->tm_min,tm->tm_sec); break; default: reply(550,"No MDTM available: %s",fstree_strerror(FE_NOTPLAIN)); break; } } static void cmd_impl_MLSD(IMPL_ARGS) { FSTREE_CSTRING as; FSTREE_OF *d; FSTREE_STAT stb; int err; ES s; long int o; int n; int i; FSTREE_DIRENT e; FSTREE_CSTRING en; char nbuf[1024]; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } as = (argl < 1) ? fstree_s_pl2c(".",1) : fstree_s_pl2c(arg,argl); d = fstree_open(cwd,as,&err); if (! d) { reply(501,"Can't MLSD: %s",fstree_strerror(err)); return; } stb = fstree_fstat(d); if (stb.type != FST_DIR) { fstree_close(d); reply(501,"Can't MLSD: %s",fstree_strerror((stb.type==FST_ERR)?stb.u.err.err:FE_NOTDIR)); return; } n = fstree_readdir(d,&o,0,0,0,0); if (n < 0) { fstree_close(d); reply(550,"Internal error"); error("MLSD: %s",fstree_strerror(-n)); return; } if (n > 0) { fstree_close(d); reply(550,"Internal error"); error("MLSD: first readdir returned %d",n); return; } es_init(&s); while <"dir"> (1) { n = fstree_readdir(d,&o,&e,1,&nbuf[0],sizeof(nbuf)); if (n < 0) { reply(550,"Internal error"); error("MSLD: readdir: %s",fstree_strerror(-n)); es_done(&s); fstree_close(d); return; } if (n == 0) break; for (i=1;i; } en = fstree_s_pl2c(e.name,e.namelen); stb = fstree_stat(d,en,0); switch (stb.type) { default: continue <"dir">; break; case FST_PLAIN: case FST_DIR: gen_mlsx_line(&s,&stb,&en); es_append_n(&s,"\r\n",2); break; } } fstree_close(d); if (get_data_conn()) { es_done(&s); return; } reply(150,"OK"); i = es_len(&s); tosend = 0; aio_oq_flush(&sendq); aio_oq_queue_free(&sendq,es_take(&s),i); aio_oq_queue_special(&sendq,AIO_SPECIAL_WRITE,&done_send,0,0); send_rid = AIO_NOID; send_wid = aio_add_poll(dataconn,&aio_rwtest_never,&wtest_data,0,&w_data,0); } static void cmd_impl_MLST(IMPL_ARGS) { FSTREE_CSTRING as; FSTREE_STAT stb; ES s; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } as = (argl < 1) ? fstree_s_pl2c(".",1) : fstree_s_pl2c(arg,argl); stb = fstree_stat(cwd,as,FSTREE_STAT_NOFOLLOW); if (stb.type == FST_ERR) { reply(504,"Can't MLST: %s",fstree_strerror(stb.u.err.err)); return; } es_init(&s); gen_mlsx_line(&s,&stb,&as); reply_first(250,"MLST OK"); reply_mid("%.*s",es_len(&s),es_buf(&s)); reply_last(250,"MLST done"); } static void cmd_impl_MODE(IMPL_ARGS) { if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if (argl < 1) { reply(501,"Need an argument"); return; } if (argl > 1) { reply(501,"Junk after transfer mode"); return; } switch (arg[0]) { case 'S': case 's': reply(200,"OK"); break; case 'B': case 'b': reply(504,"Block mode not implemented"); break; case 'C': case 'c': reply(504,"Compressed mode not implemented"); break; default: reply(501,"Unrecognized transfer mode"); break; } } static void cmd_impl_NLST(IMPL_ARGS) { do_listing("NLST",0,arg,argl); } static void cmd_impl_NOOP(IMPL_ARGS) { if (argl) { reply(501,"NOOP takes no arguments"); } else { reply(200,"OK"); } } static void cmd_OPTS_MLST(const unsigned char *arg, int argl) { int f0; ES e; int x; int i; unsigned int m; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if (argl < 1) { mlsx_mask = 0; } else { m = 0; f0 = -1; ensure_fact_lengths(); for (x=0;x=0;i--) { if ((mlsx_facts[i].namelen == x-f0) && !strncasecmp(mlsx_facts[i].name,arg+f0,x-f0)) { m |= mlsx_facts[i].bit; break; } } f0 = -1; } } else { if (f0 < 0) f0 = x; } } if (f0 >= 0) { reply(501,"Syntax error in OPTS MLST feature list"); return; } mlsx_mask = m; } es_init(&e); for (i=mlsx_nfacts-1;i>=0;i--) gen_fact_opts(&e,i); if (es_len(&e) < 1) { reply(200,"OPTS MLST"); } else { reply(200,"OPTS MLST %.*s",es_len(&e),es_buf(&e)); } es_done(&e); } static void cmd_impl_OPTS(IMPL_ARGS) { int ocl; int oo0; int i; if (argl < 1) { reply(501,"OPTS requires arguments"); return; } for (i=0;(i= argl) { reply(501,"Missing options"); return; } if ((ocl == 4) && !strncasecmp(arg,"MLST",4)) { cmd_OPTS_MLST(arg+oo0,argl-oo0); } else { reply(501,"Unrecognized OPTS subcommand"); } } static void cmd_impl_PASS(IMPL_ARGS) { if (argl < 1) { reply(501,"Need password"); } else if (logged_in) { reply(530,"Already logged in"); } else { switch (login_step) { case 1: if ((argl == 15) && !bcmp(arg,"ftp@example.com",15)) { reply_first(530,"I said, please give your name as password."); reply_last(530,"Your email would do too, but that isn't anyone's email."); } else { anon_login_reply(); logged_in = 1; } break; case -1: notice("FTP LOGIN FAILED FROM %s",peer_hi.txt); quietdrop(); reply(530,"Login incorrect."); break; default: reply(503,"Specify USER first."); break; } } } // PASV is IPv4-only static void cmd_impl_PASV(IMPL_ARGS) { struct sockaddr_in sin; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if (epsv_all) { reply(550,"EPSV ALL used"); return; } if (argl >= 0) { reply(501,"Takes no arguments"); return; } if (peer_hi.sa->sa_family != AF_INET) { reply(550,"PASV usable only over IPv4"); return; } switch (dataconn_state) { case DCS_NONE: break; case DCS_PORT: case DCS_PASV: case DCS_EPRT: case DCS_EPSV: abort_send(); break; case DCS_LIVE: reply(550,"Data transfer already in progress"); return; break; default: fatal("impossible data connection state %d",(int)dataconn_state); break; } dataconn = socket(AF_INET,SOCK_STREAM,0); if (dataconn < 0) { reply(550,"Internal error"); error("PASV: socket: %s",strerror(errno)); return; } bzero(&sin,sizeof(sin)); // XXX API botch sin.sin_family = AF_INET; sin.sin_len = sizeof(sin); sin.sin_addr = ((struct sockaddr_in *)my_hi.sa)->sin_addr; if (bind(dataconn,(const void *)&sin,sizeof(sin)) < 0) { reply(550,"Internal error"); error("PASV: bind: %s",strerror(errno)); close(dataconn); dataconn = -1; return; } if (listen(dataconn,5) < 0) { reply(550,"Internal error"); error("PASV: listen: %s",strerror(errno)); close(dataconn); dataconn = -1; return; } get_addr(dataconn,&my_di,&getsockname,"getsockname","listening port"); reply(227,"%d,%d,%d,%d,%d,%d", ((const unsigned char *)&((struct sockaddr_in *)my_di.sa)->sin_addr)[0], ((const unsigned char *)&((struct sockaddr_in *)my_di.sa)->sin_addr)[1], ((const unsigned char *)&((struct sockaddr_in *)my_di.sa)->sin_addr)[2], ((const unsigned char *)&((struct sockaddr_in *)my_di.sa)->sin_addr)[3], ((const unsigned char *)&((struct sockaddr_in *)my_di.sa)->sin_port)[0], ((const unsigned char *)&((struct sockaddr_in *)my_di.sa)->sin_port)[1]); dataconn_state = DCS_PASV; } static void cmd_impl_PORT(IMPL_ARGS) { FILE *f; int av[6]; int n1; int n2; struct in_addr ia; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if (epsv_all) { reply(550,"EPSV ALL used"); return; } if (peer_hi.sa->sa_family != AF_INET) { reply(550,"PORT usable only over IPv4"); return; } f = open_str(arg,argl); n1 = -1; n2 = -1; fscanf(f,"%d,%d,%d,%d,%d,%d %n%*c%n",&av[0],&av[1],&av[2],&av[3],&av[4],&av[5],&n1,&n2); fclose(f); if ((n1 < 0) || (n2 >= 0)) { reply(501,"Syntax error in arguments"); return; } if ( (av[0] < 0) || (av[0] > 255) || (av[1] < 0) || (av[1] > 255) || (av[2] < 0) || (av[2] > 255) || (av[3] < 0) || (av[3] > 255) || (av[4] < 0) || (av[4] > 255) || (av[5] < 0) || (av[5] > 255) ) { reply(501,"Out-of-range value in arguments"); return; } ((unsigned char *)&ia)[0] = av[0]; ((unsigned char *)&ia)[1] = av[1]; ((unsigned char *)&ia)[2] = av[2]; ((unsigned char *)&ia)[3] = av[3]; if (bcmp(&ia,&((const struct sockaddr_in *)peer_hi.sa)->sin_addr,4)) { reply(550,"Address must be client address"); return; } switch (dataconn_state) { case DCS_NONE: break; case DCS_PORT: case DCS_PASV: case DCS_EPRT: case DCS_EPSV: abort_send(); break; case DCS_LIVE: reply(550,"Data transfer already in progress"); return; break; default: fatal("impossible data connection state %d",(int)dataconn_state); break; } bzero(&psa,sizeof(psa)); // XXX API botch ((struct sockaddr_in *)&psa)->sin_family = AF_INET; ((struct sockaddr_in *)&psa)->sin_len = sizeof(struct sockaddr_in); ((struct sockaddr_in *)&psa)->sin_addr = ia; ((unsigned char *)&((struct sockaddr_in *)&psa)->sin_port)[0] = av[4]; ((unsigned char *)&((struct sockaddr_in *)&psa)->sin_port)[1] = av[5]; psalen = sizeof(struct sockaddr_in); dataconn_state = DCS_PORT; reply(200,"OK"); } static void cmd_impl_QUIT(IMPL_ARGS) { if (argl >= 0) { reply(501,"Takes no arguments"); return; } reply(221,"OK"); close_input(); } static void cmd_impl_REIN(IMPL_ARGS) { if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } reply(550,"REIN not supported for guest logins"); } static void cmd_impl_RETR(IMPL_ARGS) { int e; FSTREE_STAT stb; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if (argl < 1) { reply(501,"Need an argument"); return; } tosend = fstree_open(cwd,fstree_s_pl2c(arg,argl),&e); if (! tosend) { reply(550,"%s",fstree_strerror(e)); return; } stb = fstree_fstat(tosend); if (stb.type != FST_PLAIN) { fstree_close(tosend); reply(550,"%s",fstree_strerror((stb.type==FST_ERR)?stb.u.err.err:FE_NOTPLAIN)); return; } if (get_data_conn()) return; reply(150,"OK"); aio_oq_flush(&sendq); send_rid = aio_add_block(&read_send_data,0); send_wid = aio_add_poll(dataconn,&aio_rwtest_never,&wtest_data,0,&w_data,0); } static void cmd_SITE_IDLE(const char *arg, int argl) { int i; char *t; int v; int n1; int n2; for (i=0;(i= argl) { reply(200,"Idle timeout: %d seconds",idle_timeout); return; } t = malloc(argl-i+1); bcopy(arg+i,t,argl-i); t[argl-i] = '\0'; n1 = -1; n2 = -1; sscanf(t,"%d %n%*c%n",&v,&n1,&n2); free(t); if ((n1 < 0) || (n2 >= 0)) { reply(501,"Invalid timeout value"); return; } if (v < 5) v = 5; if (v > 1800) v = 1800; idle_timeout = v; reply(200,"Idle timeout set to %d",v); } static void cmd_impl_SITE(IMPL_ARGS) { int c0; int cl; int i; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if (argl < 1) { reply(501,"Need an argument"); return; } for (i=0;(i 1) { reply(501,"Junk after structure code"); return; } switch (arg[0]) { case 'F': case 'f': reply(200,"OK"); break; case 'R': case 'r': reply(504,"Record structure not implemented"); break; case 'P': case 'p': reply(504,"Page structure not implemented"); break; default: reply(501,"Unrecognized structure code"); break; } } static void cmd_impl_SYST(IMPL_ARGS) { reply(215,"UNIX-BSD"); } static void cmd_impl_TYPE(IMPL_ARGS) { int i; int l1; int a2; int l2; int v; if (! logged_in) { reply(530,"Login first with USER and PASS"); return; } if (argl < 1) { reply(501,"Need argument(s)"); return; } for (i=0;(i= 0) { switch (l2) { case 0: reply(501,"Junk after type code"); return; break; case 1: switch (arg[a2]) { case 'N': case 'n': break; default: reply(504,"Unimplemented ASCII format"); return; break; } break; default: reply(501,"Format too long"); return; break; } } type = XT_ASCII; reply(200,"OK"); return; break; case 'E': case 'e': reply(504,"EBCDIC not supported"); return; break; case 'I': case 'i': if (a2 >= 0) { reply(501,"Junk after type code"); return; } type = XT_IMAGE; reply(200,"OK"); return; break; case 'L': case 'l': if (a2 < 0) { reply(501,"Missing byte-size parameter"); return; } v = 0; for (i=0;i 99) v = -100; } if (v != 8) { reply(504,"Byte-size value not implemented"); return; } type = XT_LOCAL_8; reply(200,"OK"); return; break; default: reply(501,"Unrecognized type"); return; break; } abort(); } static void cmd_impl_USER(IMPL_ARGS) { if (argl < 1) { reply(501,"Need username"); } else if (logged_in) { reply(530,"Already logged in"); } else if (login_step != 0) { reply(530,"Already specified USER"); } else if ((argl == 9) && !bcmp(arg,"anonymous",9)) { login_step = 1; reply(331,"Guest login OK, please give your name as password"); } else { login_step = -1; reply(331,"Password required"); } } /* * This list must be sorted by command length first, then strncasecmp() * order for each length. */ static const CMD cmds[] = { CMD_NORMAL(CWD), CMD_WRITE(MKD), CMD_NORMAL(PWD), CMD_WRITE(RMD), CMD_NORMAL(ABOR), CMD_UNIMP(ACCT), CMD_WRITE(ALLO), CMD_WRITE(APPE), CMD_NORMAL(CDUP), CMD_WRITE(DELE), CMD_NORMAL(EPRT), CMD_NORMAL(EPSV), CMD_NORMAL(FEAT), CMD_NORMAL(HELP), CMD_NORMAL(LANG), CMD_NORMAL(LIST), CMD_NORMAL(MDTM), CMD_NORMAL(MLSD), CMD_NORMAL(MLST), CMD_NORMAL(MODE), CMD_NORMAL(NLST), CMD_NORMAL(NOOP), CMD_NORMAL(OPTS), CMD_NORMAL(PASS), CMD_NORMAL(PASV), CMD_NORMAL(PORT), CMD_NORMAL(QUIT), CMD_NORMAL(REIN), CMD_UNIMP(REST), CMD_NORMAL(RETR), CMD_WRITE(RNFR), CMD_WRITE(RNTO), CMD_NORMAL(SITE), CMD_NORMAL(SIZE), CMD_WRITE(SMNT), CMD_NORMAL(STAT), CMD_WRITE(STOR), CMD_WRITE(STOU), CMD_NORMAL(STRU), CMD_NORMAL(SYST), CMD_NORMAL(TYPE), CMD_NORMAL(USER), }; #define N_CMDS (sizeof(cmds)/sizeof(cmds[0])) static int cmd_cmp(const CMD *c, const unsigned char *s, int l) { if (c->namelen < l) return(-1); if (c->namelen > l) return(1); return(strncasecmp(c->name,s,l)); } static void process_command(const unsigned char *b, int bl) { int i; int cmdl; void (*impl)(const unsigned char *, int, const unsigned char *, int); const unsigned char *arg; int argl; int l; int m; int h; static int checked_cmds = 0; if (! checked_cmds) { for (i=N_CMDS;i>0;i--) { if (cmd_cmp(&cmds[i-1],cmds[i].name,cmds[i].namelen) >= 0) abort(); } checked_cmds = 1; } activity(); info("command: %.*s",bl,b); for (cmdl=0;(cmdl 1) { m = (h + l) / 2; i = cmd_cmp(&cmds[m],b,cmdl); if (i <= 0) l = m; else h = m; } if ((l >= 0) && !cmd_cmp(&cmds[l],b,cmdl)) impl = cmds[l].impl; (*impl)(b,cmdl,arg,argl); new_command(); } static void rd_in(void *arg __attribute__((__unused__))) { unsigned char rbuf[512]; int nr; int i; int l; unsigned char *b; int c; nr = read(0,&rbuf[0],sizeof(rbuf)); if (nr < 0) { switch (errno) { case EINTR: case EWOULDBLOCK: return; break; } fatal("command read: %s",strerror(errno)); } if (nr == 0) { info("command read EOF"); perform_abort(); close_input(); return; } if (es_len(&cmdline) > 1024) fatal("unreasonably long command line"); for (i=0;i 1) { b = es_buf(&cmdline); if (b[l-2] == 13) { process_command(b,l-2); if (in_id == AIO_NOID) return; } } } } } static int wtest_out(void *arg __attribute__((__unused__))) { return(aio_oq_nonempty(&out_oq)); } static void wr_out(void *arg __attribute__((__unused__))) { if (std_out(&out_oq,1) < 0) fatal("stdout write: %s",strerror(errno)); } static int check_idle(void *arg __attribute__((__unused__))) { time_t now; time_t idle_end; now = time(0); idle_end = last_activity + idle_timeout; if (now > idle_end) { reply(421,"Timeout, closing connection."); close_input(); aio_remove_block(idle_id); idle_id = AIO_NOID; return(AIO_BLOCK_LOOP); } return((idle_end+1-now)*1000); } static void setup(void) { openlog("ftpd",LOG_NDELAY|LOG_PID,LOG_FTP); setup_defaults(); open_stdio(); get_ctl_addrs(); fs_init(); info("startup for %s <-> %s",my_hi.txt,peer_hi.txt); telnet_init(&cmdtns); es_init(&cmdline); aio_poll_init(); aio_oq_init(&out_oq); new_command(); logged_in = 0; login_step = 0; epsv_all = 0; dataconn_state = DCS_NONE; aio_oq_init(&sendq); send_rid = AIO_NOID; send_wid = AIO_NOID; nbio(0); in_id = aio_add_poll(0,&aio_rwtest_always,&aio_rwtest_never,&rd_in,0,0); done_id = AIO_NOID; nbio(1); activity(); idle_id = aio_add_block(&check_idle,0); out_id = aio_add_poll(1,&aio_rwtest_never,&wtest_out,0,&wr_out,0); reply(220,"%s FTP server ready",myname); } static void gen_mlsx_size(ES *e, const FSTREE_STAT *stb) { if (stb->type == FST_PLAIN) es_append_printf(e,"SIZE=%llu;",stb->u.plain.size); } static void gen_mlsx_modify(ES *e, const FSTREE_STAT *stb) { if (stb->type == FST_PLAIN) { time_t tt; struct tm *tm; tt = stb->u.plain.mtime; tm = gmtime(&tt); es_append_printf(e,"MODIFY=%04u%02u%02u%02u%02u%02u;",tm->tm_year+1900,tm->tm_mon+1,tm->tm_mday,tm->tm_hour,tm->tm_min,tm->tm_sec); } } static void gen_mlsx_type(ES *e, const FSTREE_STAT *stb) { switch (stb->type) { case FST_DIR: es_append_printf(e,"TYPE=DIR"); break; case FST_PLAIN: es_append_printf(e,"TYPE=FILE"); break; default: break; } } static MLSX_FACT mlsx_facts[] = { { "size", FACT_SIZE, &gen_mlsx_size }, { "modify", FACT_MODIFY, &gen_mlsx_modify }, { "type", FACT_TYPE, &gen_mlsx_type } }; static const int mlsx_nfacts = sizeof(mlsx_facts) / sizeof(mlsx_facts[0]); int main(int, char **); int main(int ac, char **av) { handleargs(ac,av); setup(); aio_event_loop(); exit(1); }