]> git.lizzy.rs Git - plan9front.git/commitdiff
ip/ftpd: Add explict and implicit FTPS support.
authorfoura <james@biobuf.link>
Sun, 2 May 2021 14:29:43 +0000 (15:29 +0100)
committerfoura <james@biobuf.link>
Sun, 2 May 2021 14:29:43 +0000 (15:29 +0100)
Removed:
- Challenge reponse auth.
- Noworld login.
- Anonymous users writing files to /incoming.

sys/man/8/ipserv
sys/src/cmd/ip/ftpd.c

index 770d02b100a2cc6b5d36c31ccb944ae59439ff68..db934e4812a8985abb8cbf4eff521663d1062ff9 100644 (file)
@@ -12,9 +12,11 @@ telnetd, rlogind, rexexec, ftpd, socksd, hproxy \- Internet remote access daemon
 .B ip/rexexec
 .PP
 .B ip/ftpd
-.RB [ -aAde ]
+.RB [ -aAdei ]
 .RB [ -n
 .IR namepace-file ]
+.RB [ -c
+.IR cert-path ]
 .PP
 .B ip/socksd
 [
@@ -113,32 +115,20 @@ standard Plan 9 authentication (see
 .IR authsrv (6)).
 .PP
 .I Ftpd
-runs the Internet file transfer protocol.  Users may transfer
+runs the Internet file transfer protocol.  It supports both 
+implicit and explicit ftps. Users may transfer
 files in either direction between the local and
 remote machines.
-As for
-.IR telnetd ,
-there are three types of login:
-.TF anonymo
+There are two types of login:
+.TF anonymous
 .TP
 .I normal
-Normal users authenticate
-via the same challenge/response as for
-.IR telnetd .
+Normal users authenticate with their username and password when using tls.
 .BI /usr/ username /lib/namespace.ftp
 or, if that file does not exist,
 .B /lib/namespace
 defines the namespace.
 .TP
-.I noworld
-Users in group
-.B noworld
-in
-.B /adm/users
-login using a password in the clear.
-.B /lib/namespace.noworld
-defines the namespace.
-.TP
 .I anonymous
 Users
 .B anonymous
@@ -150,9 +140,7 @@ The argument to the
 option (default
 .IR /lib/namespace.ftp )
 defines the namespace.
-Anonymous users may only store files in the subtree
-below
-.BR /incoming .
+Anonymous users may not store files.
 .PD
 .PP
 .IR Ftpd 's
@@ -167,23 +155,18 @@ allow
 anonymous access
 .TP
 .B d
-write debugging output to standard error
+write debugging output to the log
 .TP
 .B e
 treat any user as anonymous
 .TP
+.B c
+the certificate to use for serving ftps. The key must be stored in factotum.
+.TP
 .B n
 the namespace for anonymous users (default
 .BR /lib/namespace.ftp )
 .PP
-To preserve intended protections in shared file trees,
-any directory containing a file
-.I .httplogin
-is locked by
-.IR ftpd;
-see
-.IR httpd (8).
-.PP
 .I Socksd
 is a SOCKS4 and SOCKS5
 proxy server allowing non Plan9 machines to access the
index 6661189e9965a4435a25ab330e5be89133fe8147..7519bc6bc13a071ba3779ee925d52d68cb42416e 100644 (file)
 #include <u.h>
 #include <libc.h>
 #include <bio.h>
-#include <auth.h>
 #include <ip.h>
 #include <libsec.h>
-#include <String.h>
+#include <auth.h>
 
+#include <String.h>
 #include "glob.h"
 
-enum
-{
-       /* telnet control character */
-       Iac=            255,
-
-       /* representation types */
-       Tascii=         0,
-       Timage=         1,
-
-       /* transmission modes */
-       Mstream=        0,
-       Mblock=         1,
-       Mpage=          2,
-
-       /* file structure */
-       Sfile=          0,
-       Sblock=         1,
-       Scompressed=    2,
+enum {
+       Tascii,
+       Timage,
 
-       /* read/write buffer size */
-       Nbuf=           4096,
-
-       /* maximum ms we'll wait for a command */
-       Maxwait=        1000*60*30,             /* inactive for 30 minutes, we hang up */
-
-       Maxpath=        512,
+       Maxpath = 512,
+       Maxwait = 1000 * 60 * 30, /* 30 minutes */
 };
 
-int    abortcmd(char*);
-int    appendcmd(char*);
-int    cdupcmd(char*);
-int    cwdcmd(char*);
-int    delcmd(char*);
-int    helpcmd(char*);
-int    listcmd(char*);
-int    mdtmcmd(char*);
-int    mkdircmd(char*);
-int    modecmd(char*);
-int    namelistcmd(char*);
-int    nopcmd(char*);
-int    optscmd(char*);
-int    passcmd(char*);
-int    pasvcmd(char*);
-int    portcmd(char*);
-int    pwdcmd(char*);
-int    quitcmd(char*);
-int    rnfrcmd(char*);
-int    rntocmd(char*);
-int    reply(char*, ...);
-int    restartcmd(char*);
-int    retrievecmd(char*);
-int    sitecmd(char*);
-int    sizecmd(char*);
-int    storecmd(char*);
-int    storeucmd(char*);
-int    structcmd(char*);
-int    systemcmd(char*);
-int    typecmd(char*);
-int    usercmd(char*);
-
-int    dialdata(void);
-char*  abspath(char*);
-int    crlfwrite(int, char*, int);
-int    sodoff(void);
-int    accessok(char*);
-
-typedef struct Cmd     Cmd;
-struct Cmd
-{
-       char    *name;
-       int     (*f)(char*);
-       int     needlogin;
+typedef struct Passive Passive;
+typedef struct Ftpd Ftpd;
+typedef struct Cmd Cmd;
+
+struct Passive {
+       int inuse;
+       char adir[40];
+       int afd;
+       int port;
+       uchar ipaddr[IPaddrlen];
 };
 
-Cmd cmdtab[] =
-{
-       { "abor",       abortcmd,       0, },
-       { "allo",       nopcmd,         1, },
-       { "appe",       appendcmd,      1, },
-       { "cdup",       cdupcmd,        1, },
-       { "cwd",        cwdcmd,         1, },
-       { "dele",       delcmd,         1, },
-       { "help",       helpcmd,        0, },
-       { "list",       listcmd,        1, },
-       { "mdtm",       mdtmcmd,        1, },
-       { "mkd",        mkdircmd,       1, },
-       { "mode",       modecmd,        0, },
-       { "nlst",       namelistcmd,    1, },
-       { "noop",       nopcmd,         0, },
-       { "opts",       optscmd,        0, },
-       { "pass",       passcmd,        0, },
-       { "pasv",       pasvcmd,        1, },
-       { "pwd",        pwdcmd,         0, },
-       { "port",       portcmd,        1, },
-       { "quit",       quitcmd,        0, },
-       { "rest",       restartcmd,     1, },
-       { "retr",       retrievecmd,    1, },
-       { "rmd",        delcmd,         1, },
-       { "rnfr",       rnfrcmd,        1, },
-       { "rnto",       rntocmd,        1, },
-       { "site", sitecmd, 1, },
-       { "size",       sizecmd,        1, },
-       { "stor",       storecmd,       1, },
-       { "stou",       storeucmd,      1, },
-       { "stru",       structcmd,      1, },
-       { "syst",       systemcmd,      0, },
-       { "type",       typecmd,        0, },
-       { "user",       usercmd,        0, },
-       { 0, 0, 0 },
+struct Ftpd {
+       Biobuf *in, *out;
+
+       struct conn {
+               int tlson, tlsondata;
+               NetConnInfo *nci;
+               TLSconn *tls;
+               uchar *cert;
+               int certlen;
+               char data[64];
+               Passive pasv;
+       } conn;
+
+       struct user {
+               char cwd[Maxpath];
+               char name[Maxpath];
+               int loggedin;
+               int isnone;
+       } user;
+
+       int type;
+       vlong offset;
+       int cmdpid;
+       char *renamefrom;
 };
 
-#define NONENS "/lib/namespace.ftp"    /* default ns for none */
-
-char   user[Maxpath];          /* logged in user */
-char   curdir[Maxpath];        /* current directory path */
-Chalstate      *ch;
-int    loggedin;
-int    type;                   /* transmission type */
-int    mode;                   /* transmission mode */
-int    structure;              /* file structure */
-char   data[64];               /* data address */
-int    pid;                    /* transfer process */
-int    encryption;             /* encryption state */
-int    isnone, anon_ok, anon_only, anon_everybody;
-char   cputype[Maxpath];       /* the environment variable of the same name */
-char   bindir[Maxpath];        /* bin directory for this architecture */
-char   mailaddr[Maxpath];
-char   *namespace = NONENS;
-int    debug;
-NetConnInfo    *nci;
-int    createperm = 0660;
-int    isnoworld;
-vlong  offset;                 /* from restart command */
-
-ulong id;
-
-typedef struct Passive Passive;
-struct Passive
-{
-       int     inuse;
-       char    adir[40];
-       int     afd;
-       int     port;
-       uchar   ipaddr[IPaddrlen];
-} passive;
+struct Cmd {
+       char *name;
+       int (*fn)(Ftpd *, char *);
+       int needlogin;
+       int needtls;
+       int asproc;
+};
 
-#define FTPLOG "ftp"
+char *certpath;
+char *namespace = "/lib/namespace.ftp";
+int implicittls;
+int debug;
+int anonok;
+int anononly;
+int anonall;
 
-void
-logit(char *fmt, ...)
+void 
+dprint(char *fmt, ...)
 {
-       char buf[8192];
+       char *msg;
        va_list arg;
 
+       if(!debug) return;
+
        va_start(arg, fmt);
-       vseprint(buf, buf+sizeof(buf), fmt, arg);
+       msg = vsmprint(fmt, arg);
        va_end(arg);
-       syslog(0, FTPLOG, "%s.%s %s", nci->rsys, nci->rserv, buf);
-}
 
-static void
-usage(void)
-{
-       syslog(0, "ftp", "usage: %s [-aAde] [-n nsfile]", argv0);
-       fprint(2, "usage: %s [-aAde] [-n nsfile]\n", argv0);
-       exits("usage");
+       syslog(0, "ftp", msg);
+       free(msg);
 }
 
-/*
- *  read commands from the control stream and dispatch
- */
-void
-main(int argc, char **argv)
+void 
+logit(char *fmt, ...)
 {
-       char *cmd;
-       char *arg;
-       char *p;
-       Cmd *t;
-       Biobuf in;
-       int i;
-
-       ARGBEGIN{
-       case 'a':               /* anonymous OK */
-               anon_ok = 1;
-               break;
-       case 'A':
-               anon_ok = 1;
-               anon_only = 1;
-               break;
-       case 'd':
-               debug++;
-               break;
-       case 'e':
-               anon_ok = 1;
-               anon_everybody = 1;
-               break;
-       case 'n':
-               namespace = EARGF(usage());
-               break;
-       default:
-               usage();
-       }ARGEND
-
-       /* open log file before doing a newns */
-       syslog(0, FTPLOG, nil);
-
-       /* find out who is calling */
-       if(argc < 1)
-               nci = getnetconninfo(nil, 0);
-       else
-               nci = getnetconninfo(argv[argc-1], 0);
-       if(nci == nil)
-               sysfatal("ftpd needs a network address");
-
-       strcpy(mailaddr, "?");
-       id = getpid();
-
-       /* figure out which binaries to bind in later (only for none) */
-       arg = getenv("cputype");
-       if(arg)
-               strecpy(cputype, cputype+sizeof cputype, arg);
-       else
-               strcpy(cputype, "mips");
-       /* shurely /%s/bin */
-       snprint(bindir, sizeof(bindir), "/bin/%s/bin", cputype);
-
-       Binit(&in, 0, OREAD);
-       reply("220 Plan 9 FTP server ready");
-       alarm(Maxwait);
-       while(cmd = Brdline(&in, '\n')){
-               alarm(0);
-
-               /*
-                *  strip out trailing cr's & lf and delimit with null
-                */
-               i = Blinelen(&in)-1;
-               cmd[i] = 0;
-               if(debug)
-                       logit("%s", cmd);
-               while(i > 0 && cmd[i-1] == '\r')
-                       cmd[--i] = 0;
-
-               /*
-                *  hack for GatorFTP+, look for a 0x10 used as a delimiter
-                */
-               p = strchr(cmd, 0x10);
-               if(p)
-                       *p = 0;
-
-               /*
-                *  get rid of telnet control sequences (we don't need them)
-                */
-               while(*cmd && (uchar)*cmd == Iac){
-                       cmd++;
-                       if(*cmd)
-                               cmd++;
-               }
-
-               /*
-                *  parse the message (command arg)
-                */
-               arg = strchr(cmd, ' ');
-               if(arg){
-                       *arg++ = 0;
-                       while(*arg == ' ')
-                               arg++;
-               }
+       char *msg;
+       va_list arg;
 
-               /*
-                *  ignore blank commands
-                */
-               if(*cmd == 0)
-                       continue;
+       va_start(arg, fmt);
+       msg = vsmprint(fmt, arg);
+       va_end(arg);
 
-               /*
-                *  lookup the command and do it
-                */
-               for(p = cmd; *p; p++)
-                       *p = tolower(*p);
-               for(t = cmdtab; t->name; t++)
-                       if(strcmp(cmd, t->name) == 0){
-                               if(t->needlogin && !loggedin)
-                                       sodoff();
-                               else if((*t->f)(arg) < 0)
-                                       exits(0);
-                               break;
-                       }
-               if(t->f != restartcmd){
-                       /*
-                        *  the file offset is set to zero following
-                        *  all commands except the restart command
-                        */
-                       offset = 0;
-               }
-               if(t->name == 0){
-                       /*
-                        *  the OOB bytes preceding an abort from UCB machines
-                        *  comes out as something unrecognizable instead of
-                        *  IAC's.  Certainly a Plan 9 bug but I can't find it.
-                        *  This is a major hack to avoid the problem. -- presotto
-                        */
-                       i = strlen(cmd);
-                       if(i > 4 && strcmp(cmd+i-4, "abor") == 0){
-                               abortcmd(0);
-                       } else{
-                               logit("%s (%s) command not implemented", cmd, arg?arg:"");
-                               reply("502 %s command not implemented", cmd);
-                       }
-               }
-               alarm(Maxwait);
-       }
-       if(pid)
-               postnote(PNPROC, pid, "kill");
+       syslog(0, "ftp", msg);
+       free(msg);
 }
 
-/*
- *  reply to a command
- */
-int
-reply(char *fmt, ...)
+int 
+reply(Biobuf *bio, char *fmt, ...)
 {
        va_list arg;
-       char buf[8192], *s;
+       char buf[Maxpath], *s;
 
        va_start(arg, fmt);
-       s = vseprint(buf, buf+sizeof(buf)-3, fmt, arg);
+       s = vseprint(buf, buf + sizeof(buf) - 3, fmt, arg);
        va_end(arg);
-       if(debug){
-               *s = 0;
-               logit("%s", buf);
-       }
+
+       dprint("rpl: %s", buf);
+
        *s++ = '\r';
        *s++ = '\n';
-       write(1, buf, s - buf);
-       return 0;
-}
+       Bwrite(bio, buf, s - buf);
+       Bflush(bio);
 
-int
-sodoff(void)
-{
-       return reply("530 Sod off, service requires login");
+       return 0;
 }
 
-/*
- *  run a command in a separate process
- */
-int
-asproc(void (*f)(char*, int), char *arg, int arg2)
+void
+asproc(Ftpd *ftpd, int (*f)(Ftpd *, char *), char *arg)
 {
        int i;
 
-       if(pid){
-               /* wait for previous command to finish */
-               for(;;){
+       if(ftpd->cmdpid) {
+               for(;;) {
                        i = waitpid();
-                       if(i == pid || i < 0)
+                       if(i == ftpd->cmdpid || i < 0)
                                break;
                }
        }
 
-       switch(pid = rfork(RFFDG|RFPROC|RFNOTEG)){
+       switch(ftpd->cmdpid = rfork(RFFDG|RFPROC|RFNOTEG)){
        case -1:
-               return reply("450 Out of processes: %r");
+               reply(ftpd->out, "450 Out of processes: %r");
+               return;
        case 0:
-               (*f)(arg, arg2);
-               exits(0);
+               (*f)(ftpd, arg);
+               dprint("proc exiting");
+               exits(nil);
        default:
                break;
        }
+}
+
+int 
+mountnet(Ftpd *ftpd)
+{
+       if(bind("#/", "/", MAFTER) == -1) {
+               reply(ftpd->out, "500 can't bind #/ to /: %r");
+               return -1;
+       }
+
+       if(bind(ftpd->conn.nci->spec, "/net", MBEFORE) == -1) {
+               reply(ftpd->out, "500 can't bind %s to /net: %r", ftpd->conn.nci->spec);
+               unmount("#/", "/");
+               return -1;
+       }
+
        return 0;
 }
 
-/*
- * run a command to filter a tail
- */
-int
-transfer(char *cmd, char *a1, char *a2, char *a3, int image)
+void 
+unmountnet(void)
 {
-       int n, dfd, fd, bytes, eofs, pid;
-       int pfd[2];
-       char buf[Nbuf], *p;
-       Waitmsg *w;
+       unmount(nil, "/net");
+       unmount("#/", "/");
+}
 
-       reply("150 Opening data connection for %s (%s)", cmd, data);
-       dfd = dialdata();
-       if(dfd < 0)
-               return reply("425 Error opening data connection: %r");
+Biobuf *
+dialdata(Ftpd *ftpd, int read)
+{
+       Biobuf *bio;
+       TLSconn *tls;
+       int fd, cfd;
+       char ldir[40];
 
-       if(pipe(pfd) < 0)
-               return reply("520 Internal Error: %r");
+       if(mountnet(ftpd) < 0)
+               return nil;
 
-       bytes = 0;
-       switch(pid = rfork(RFFDG|RFPROC|RFNAMEG)){
-       case -1:
-               return reply("450 Out of processes: %r");
-       case 0:
-               logit("running %s %s %s %s pid %d",
-                       cmd, a1?a1:"", a2?a2:"" , a3?a3:"",getpid());
-               close(pfd[1]);
-               close(dfd);
-               dup(pfd[0], 1);
-               dup(pfd[0], 2);
-               if(isnone){
-                       fd = open("#s/boot", ORDWR);
-                       if(fd < 0
-                       || bind("#/", "/", MAFTER) == -1
-                       || amount(fd, "/bin", MREPL, "") == -1
-                       || bind("#c", "/dev", MAFTER) == -1
-                       || bind(bindir, "/bin", MREPL) == -1)
-                               exits("building name space");
-                       close(fd);
-               }
-               execl(cmd, cmd, a1, a2, a3, nil);
-               exits(cmd);
-       default:
-               close(pfd[0]);
-               eofs = 0;
-               while((n = read(pfd[1], buf, sizeof buf)) >= 0){
-                       if(n == 0){
-                               if(eofs++ > 5)
-                                       break;
-                               else
-                                       continue;
-                       }
-                       eofs = 0;
-                       p = buf;
-                       if(offset > 0){
-                               if(n > offset){
-                                       p = buf+offset;
-                                       n -= offset;
-                                       offset = 0;
-                               } else {
-                                       offset -= n;
-                                       continue;
-                               }
-                       }
-                       if(!image)
-                               n = crlfwrite(dfd, p, n);
-                       else
-                               n = write(dfd, p, n);
-                       if(n < 0){
-                               postnote(PNPROC, pid, "kill");
-                               bytes = -1;
-                               break;
-                       }
-                       bytes += n;
+       if(!ftpd->conn.pasv.inuse) {
+               fd = dial(ftpd->conn.data, "20", 0, 0);
+       } else {
+               fd = -1;
+               alarm(30 * 1000); /* wait 30 seconds */
+               dprint("dbg: waiting for passive connection");
+               cfd = listen(ftpd->conn.pasv.adir, ldir);
+               alarm(0);
+
+               if(cfd >= 0) {
+                       fd = accept(cfd, ldir);
+                       close(cfd);
                }
-               close(pfd[1]);
-               close(dfd);
-               break;
        }
 
-       /* wait for this command to finish */
-       for(;;){
-               w = wait();
-               if(w == nil || w->pid == pid)
-                       break;
-               free(w);
-       }
-       if(w != nil && w->msg != nil && w->msg[0] != 0){
-               bytes = -1;
-               logit("%s", w->msg);
-               logit("%s %s %s %s failed %s", cmd, a1?a1:"", a2?a2:"" , a3?a3:"", w->msg);
+       if(fd < 0) {
+               reply(ftpd->out, "425 Error opening data connection");
+               unmountnet();
+               return nil;
        }
-       free(w);
-       reply("226 Transfer complete");
-       return bytes;
-}
 
-int
-optscmd(char *arg)
-{
-       char *p;
+       reply(ftpd->out, "150 Opened data connection");
 
-       if(arg == 0 || *arg == 0){
-               reply("501 Syntax error in parameters or arguments");
-               return 0;
-       }
-       if(p = strchr(arg, ' '))
-               *p = 0;
-       if(cistrcmp(arg, "UTF-8") == 0 || cistrcmp(arg, "UTF8") == 0){
-               reply("200 Command okay");
-               return 0;
+       tls = nil;
+       if(ftpd->conn.tlsondata) {
+               dprint("dbg: using tls on data channel");
+
+               tls = mallocz(sizeof(TLSconn), 1);
+               tls->cert = malloc(ftpd->conn.certlen);
+               memcpy(tls->cert, ftpd->conn.cert, ftpd->conn.certlen);
+               tls->certlen = ftpd->conn.certlen;
+               fd = tlsServer(fd, tls);
+
+               if(fd < 0) {
+                       reply(ftpd->out, "425 TLS on data connection failed");
+                       unmountnet();
+                       return nil;
+               }
+
+               dprint("dbg: tlsserver done");
        }
-       reply("502 %s option not implemented", arg);
-       return 0;
-}
 
-/*
- *  just reply OK
- */
-int
-nopcmd(char *arg)
-{
-       USED(arg);
-       reply("510 Plan 9 FTP daemon still alive");
-       return 0;
+       unmountnet();
+       if(read)
+               bio = Bfdopen(fd, OREAD);
+       else
+               bio = Bfdopen(fd, OWRITE);
+       bio->aux = tls;
+
+       return bio;
 }
 
-/*
- *  login as user
- */
-int
-loginuser(char *user, char *nsfile, int gotoslash)
+void 
+closedata(Ftpd *ftpd, Biobuf *bio, int fail)
 {
-       logit("login %s %s %s %s", user, mailaddr, nci->rsys, nsfile);
-       if(nsfile != nil && newns(user, nsfile) < 0){
-               logit("namespace file %s does not exist", nsfile);
-               return reply("530 Not logged in: login out of service");
-       }
-       getwd(curdir, sizeof(curdir));
-       if(gotoslash){
-               chdir("/");
-               strcpy(curdir, "/");
+       TLSconn *conn;
+
+       conn = bio->aux;
+
+       Bflush(bio);
+       Bterm(bio);
+       if(!fail)
+               reply(ftpd->out, "226 Transfer complete");
+
+       if(conn) {
+               free(conn->cert);
+               free(conn);
        }
-       putenv("service", "ftp");
-       loggedin = 1;
-       if(debug == 0)
-               reply("230- If you have problems, send mail to 'postmaster'.");
-       return reply("230 Logged in");
 }
 
-static void
-slowdown(void)
+int 
+starttls(Ftpd *ftpd)
 {
-       static ulong pause;
-
-       if (pause) {
-               sleep(pause);                   /* deter guessers */
-               if (pause < (1UL << 20))
-                       pause *= 2;
-       } else
-               pause = 1000;
+       int fd;
+
+       fd = tlsServer(0, ftpd->conn.tls);
+       if(fd < 0)
+               return -1;
+
+       dup(fd, 0);
+       dup(fd, 1);
+       ftpd->conn.tlson = 1;
+
+       return 0;
 }
 
-/*
- *  get a user id, reply with a challenge.  The users 'anonymous'
- *  and 'ftp' are equivalent to 'none'.  The user 'none' requires
- *  no challenge.
- */
 int
-usercmd(char *name)
+abortcmd(Ftpd *ftpd, char *arg)
 {
-       slowdown();
-
-       logit("user %s %s", name, nci->rsys);
-       if(loggedin)
-               return reply("530 Already logged in as %s", user);
-       if(name == 0 || *name == 0)
-               return reply("530 user command needs user name");
-       isnoworld = 0;
-       if(*name == ':'){
-               debug = 1;
-               name++;
-       }
-       strncpy(user, name, sizeof(user));
-       if(debug)
-               logit("debugging");
-       user[sizeof(user)-1] = 0;
-       if(strcmp(user, "anonymous") == 0 || strcmp(user, "ftp") == 0)
-               strcpy(user, "none");
-       else if(anon_everybody)
-               strcpy(user,"none");
-
-       if(strcmp(user, "Administrator") == 0 || strcmp(user, "admin") == 0)
-               return reply("530 go away, script kiddie");
-       else if(strcmp(user, "*none") == 0){
-               if(!anon_ok)
-                       return reply("530 Not logged in: anonymous disallowed");
-               return loginuser("none", namespace, 1);
-       }
-       else if(strcmp(user, "none") == 0){
-               if(!anon_ok)
-                       return reply("530 Not logged in: anonymous disallowed");
-               return reply("331 Send email address as password");
+       USED(arg);
+
+       if(ftpd->cmdpid){
+               if(postnote(PNPROC, ftpd->cmdpid, "kill") == 0)
+                       reply(ftpd->out, "426 Command aborted");
+               else
+                       logit("postnote pid %d %r", ftpd->cmdpid);
        }
-       else if(anon_only)
-               return reply("530 Not logged in: anonymous access only");
-
-       isnoworld = noworld(name);
-       if(isnoworld)
-               return reply("331 OK");
-
-       /* consult the auth server */
-       if(ch)
-               auth_freechal(ch);
-       if((ch = auth_challenge("proto=p9cr role=server user=%q", user)) == nil)
-               return reply("421 %r");
-       return reply("331 encrypt challenge, %s, as a password", ch->chal);
+       return reply(ftpd->out, "226 Abort processed");
 }
 
-/*
- *  get a password, set up user if it works.
- */
-int
-passcmd(char *response)
+int 
+authcmd(Ftpd *ftpd, char *arg)
 {
-       char namefile[128];
-       AuthInfo *ai;
-       Dir nd;
-
-       if(response == nil)
-               response = "";
-
-       if(strcmp(user, "none") == 0 || strcmp(user, "*none") == 0){
-               /* for none, accept anything as a password */
-               isnone = 1;
-               strncpy(mailaddr, response, sizeof(mailaddr)-1);
-               return loginuser("none", namespace, 1);
-       }
+       if((cistrcmp(arg, "TLS") == 0) || (cistrcmp(arg, "TLS-C") == 0) || (cistrcmp(arg, "SSL") == 0)) {
+
+               if(!ftpd->conn.tls)
+                       return reply(ftpd->out, "431 tls not enabled");
 
-       if(isnoworld){
-               /* noworld gets a password in the clear */
-               if(login(user, response, "/lib/namespace.noworld") < 0)
-                       return reply("530 Not logged in");
-               createperm = 0664;
-               /* login has already setup the namespace */
-               return loginuser(user, nil, 0);
+               reply(ftpd->out, "234 starting tls");
+               if(starttls(ftpd) < 0)
+                       return reply(ftpd->out, "431 tls failed");
        } else {
-               /* for everyone else, do challenge response */
-               if(ch == nil)
-                       return reply("531 Send user id before encrypted challenge");
-               ch->resp = response;
-               ch->nresp = strlen(response);
-               ai = auth_response(ch);
-               if(ai == nil || auth_chuid(ai, nil) < 0) {
-                       auth_freeAI(ai);
-                       slowdown();
-                       return reply("530 Not logged in: %r");
-               }
-               /* chown network connection */
-               nulldir(&nd);
-               nd.mode = 0660;
-               nd.uid = ai->cuid;
-               dirfwstat(0, &nd);
-
-               auth_freeAI(ai);
-               auth_freechal(ch);
-               ch = nil;
-
-               /* if the user has specified a namespace for ftp, use it */
-               snprint(namefile, sizeof(namefile), "/usr/%s/lib/namespace.ftp", user);
-               strcpy(mailaddr, user);
-               createperm = 0660;
-               if(access(namefile, 0) == 0)
-                       return loginuser(user, namefile, 0);
-               else
-                       return loginuser(user, "/lib/namespace", 0);
+               return reply(ftpd->out, "502 security method %s not understood", arg);
        }
-}
 
-/*
- *  print working directory
- */
-int
-pwdcmd(char *arg)
-{
-       if(arg)
-               return reply("550 Pwd takes no argument");
-       return reply("257 \"%s\" is the current directory", curdir);
+       return 0;
 }
 
-/*
- *  chdir
- */
-int
-cwdcmd(char *dir)
+int 
+cwdcmd(Ftpd *ftpd, char *arg)
 {
-       char *rp;
        char buf[Maxpath];
 
-       /* shell cd semantics */
-       if(dir == 0 || *dir == 0){
-               if(isnone)
-                       rp = "/";
-               else {
-                       snprint(buf, sizeof buf, "/usr/%s", user);
-                       rp = buf;
-               }
-               if(accessok(rp) == 0)
-                       rp = nil;
-       } else
-               rp = abspath(dir);
-
-       if(rp == nil)
-               return reply("550 Permission denied");
-
-       if(chdir(rp) < 0)
-               return reply("550 Cwd failed: %r");
-       strcpy(curdir, rp);
-       return reply("250 directory changed to %s", curdir);
+       if(!arg || *arg == '\0') {
+               if(ftpd->user.isnone)
+                       snprint(buf, Maxpath, "/");
+               else
+                       snprint(buf, Maxpath, "/usr/%s", ftpd->user.name);
+       } else {
+               strncpy(buf, arg, Maxpath);
+               cleanname(buf);
+       }
+
+       if(chdir(buf) < 0)
+               return reply(ftpd->out, "550 CWD failed: %r");
+
+       getwd(ftpd->user.cwd, Maxpath);
+       return reply(ftpd->out, "200 Directory changed to %s", ftpd->user.cwd);
 }
 
-/*
- *  chdir ..
- */
-int
-cdupcmd(char *dp)
+int 
+deletecmd(Ftpd *ftpd, char *arg)
 {
-       USED(dp);
-       return cwdcmd("..");
+       if(!arg)
+               return reply(ftpd->out, "501 Rmdir/Delete command needs an argument");
+       if(ftpd->user.isnone)
+               return reply(ftpd->out, "550 Permission denied");
+       if(remove(cleanname(arg)) < 0)
+               return reply(ftpd->out, "550 Can't remove %s: %r", arg);
+       else
+               return reply(ftpd->out, "226 \"%s\" removed", arg);
 }
 
-int
-quitcmd(char *arg)
+int 
+featcmd(Ftpd *ftpd, char *arg)
 {
        USED(arg);
-       reply("200 Bye");
-       if(pid)
-               postnote(PNPROC, pid, "kill");
-       return -1;
+       reply(ftpd->out, "211-Features supported");
+       reply(ftpd->out, " UTF8");
+       reply(ftpd->out, " PBSZ");
+       reply(ftpd->out, " PROT");
+       reply(ftpd->out, " AUTH TLS");
+       reply(ftpd->out, " MLST Type*;Size*;Modify*;Unix.groupname*;UNIX.ownername*;");
+       return reply(ftpd->out, "211 End");
 }
 
-int
-typecmd(char *arg)
+int 
+dircmp(void *va, void *vb)
 {
-       int c;
-       char *x;
+       Dir *a, *b;
 
-       x = arg;
-       if(arg == 0)
-               return reply("501 Type command needs arguments");
+       a = va;
+       b = vb;
 
-       while(c = *arg++){
-               switch(tolower(c)){
-               case 'a':
-                       type = Tascii;
-                       break;
-               case 'i':
-               case 'l':
-                       type = Timage;
-                       break;
-               case '8':
-               case ' ':
-               case 'n':
-               case 't':
-               case 'c':
-                       break;
-               default:
-                       return reply("501 Unimplemented type %s", x);
-               }
-       }
-       return reply("200 Type %s", type==Tascii ? "Ascii" : "Image");
+       return strcmp(a->name, b->name);
 }
 
-int
-modecmd(char *arg)
+void
+listdir(Ftpd *ftpd, Biobuf *data, char *path, void (*fn)(Biobuf *, Dir *d, char *dirname))
 {
-       if(arg == 0)
-               return reply("501 Mode command needs arguments");
-       while(*arg){
-               switch(tolower(*arg)){
-               case 's':
-                       mode = Mstream;
-                       break;
-               default:
-                       return reply("501 Unimplemented mode %c", *arg);
-               }
-               arg++;
-       }
-       return reply("200 Stream mode");
+       Dir *dirbuf;
+       int fd;
+       long ndirs;
+       long i;
+
+       fd = open(path, OREAD);
+       if(!fd)
+               return;
+
+       ndirs = dirreadall(fd, &dirbuf);
+       if(ndirs < 1)
+               return;
+       close(fd);
+
+       qsort(dirbuf, ndirs, sizeof(Dir), dircmp);
+       for(i=0;i<ndirs;i++)
+               (*fn)(data, &dirbuf[i], (strcmp(path, ftpd->user.cwd) == 0 ? nil : path));
+
+       free(dirbuf);
 }
 
 int
-structcmd(char *arg)
+list(Ftpd *ftpd, char *arg, void (*fn)(Biobuf *, Dir *d, char *dirname))
 {
-       if(arg == 0)
-               return reply("501 Struct command needs arguments");
-       for(; *arg; arg++){
-               switch(tolower(*arg)){
-               case 'f':
-                       structure = Sfile;
-                       break;
-               default:
-                       return reply("501 Unimplemented structure %c", *arg);
-               }
+       Biobuf *data;
+       int argc, i;
+       char *argv[32];
+       Globlist *gl;
+       char *path;
+       Dir *d;
+
+       if(arg) {
+               argc = getfields(arg, argv, sizeof(argv)-1, 1, " \t");
+       } else {
+               argc = 1;
+               argv[0] = ftpd->user.cwd;
        }
-       return reply("200 File structure");
-}
 
-int
-portcmd(char *arg)
-{
-       char *field[7];
-       int n;
-
-       if(arg == 0)
-               return reply("501 Port command needs arguments");
-       n = getfields(arg, field, 7, 0, ", ");
-       if(n != 6)
-               return reply("501 Incorrect port specification");
-       snprint(data, sizeof data, "tcp!%.3s.%.3s.%.3s.%.3s!%d", field[0], field[1], field[2],
-               field[3], atoi(field[4])*256 + atoi(field[5]));
-       return reply("200 Data port is %s", data);
-}
+       data = dialdata(ftpd, 0);
+       if(!data)
+               return reply(ftpd->out, "500 List failed: couldn't dial data");
 
-int
-mountnet(void)
-{
-       int rv;
+       for(i=0;i<argc;i++) {
+               gl = glob(argv[i]);
+               if(!gl)
+                       continue;
 
-       rv = 0;
+               while(path = globiter(gl)) {
+                       cleanname(path);
 
-       if(bind("#/", "/", MAFTER) == -1){
-               logit("can't bind #/ to /: %r");
-               return reply("500 can't bind #/ to /: %r");
-       }
+                       logit("list: path %s user %s", path, ftpd->user.name);
 
-       if(bind(nci->spec, "/net", MBEFORE) == -1){
-               logit("can't bind %s to /net: %r", nci->spec);
-               rv = reply("500 can't bind %s to /net: %r", nci->spec);
-               unmount("#/", "/");
+                       d = dirstat(path);
+                       if(d->mode & DMDIR)
+                               listdir(ftpd, data, path, fn);
+                       else
+                               (*fn)(data, d, nil);
+
+                       free(d);
+               }
        }
 
-       return rv;
-}
+       closedata(ftpd, data, 0);
 
-void
-unmountnet(void)
-{
-       unmount(0, "/net");
-       unmount("#/", "/");
+       return 0;
 }
 
-int
-pasvcmd(char *arg)
+char *
+mode2asc(int m)
 {
-       NetConnInfo *nnci;
-       Passive *p;
-
-       USED(arg);
-       p = &passive;
-
-       if(p->inuse){
-               close(p->afd);
-               p->inuse = 0;
-       }
-
-       if(mountnet() < 0)
-               return 0;
-
-       p->afd = announce("tcp!*!0", passive.adir);
-       if(p->afd < 0){
-               unmountnet();
-               return reply("500 No free ports");
-       }
-       nnci = getnetconninfo(p->adir, -1);
-       unmountnet();
-
-       /* parse the local address */
-       if(debug)
-               logit("local sys is %s", nci->lsys);
-       parseip(p->ipaddr, nci->lsys);
-       if(ipcmp(p->ipaddr, v4prefix) == 0 || ipcmp(p->ipaddr, IPnoaddr) == 0)
-               parseip(p->ipaddr, nci->lsys);
-       p->port = atoi(nnci->lserv);
-
-       freenetconninfo(nnci);
-       p->inuse = 1;
-
-       return reply("227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)",
-               p->ipaddr[IPv4off+0], p->ipaddr[IPv4off+1], p->ipaddr[IPv4off+2], p->ipaddr[IPv4off+3],
-               p->port>>8, p->port&0xff);
-}
-
-enum
-{
-       Narg=32,
-};
-int Cflag, rflag, tflag, Rflag;
-int maxnamelen;
-int col;
-
-char*
-mode2asc(int m)
-{
-       static char asc[12];
+       char *asc;
        char *p;
 
-       strcpy(asc, "----------");
+       asc = strdup("----------");
        if(DMDIR & m)
                asc[0] = 'd';
        if(DMAPPEND & m)
@@ -895,7 +442,7 @@ mode2asc(int m)
        else if(DMEXCL & m)
                asc[3] = 'l';
 
-       for(p = asc+1; p < asc + 10; p += 3, m<<=3){
+       for(p = asc + 1; p < asc + 10; p += 3, m <<= 3) {
                if(m & 0400)
                        p[0] = 'r';
                if(m & 0200)
@@ -903,1038 +450,675 @@ mode2asc(int m)
                if(m & 0100)
                        p[2] = 'x';
        }
+
        return asc;
 }
-void
-listfile(Biobufhdr *b, char *name, int lflag, char *dname)
-{
-       char ts[32];
-       int n, links, pad;
-       long now;
-       char *x;
-       Dir *d;
 
-       x = abspath(name);
-       if(x == nil)
-               return;
-       d = dirstat(x);
-       if(d == nil)
-               return;
-       if(isnone){
-               if(strncmp(x, "/incoming/", sizeof("/incoming/")-1) != 0)
-                       d->mode &= ~0222;
-               d->uid = "none";
-               d->gid = "none";
-       }
-
-       strcpy(ts, ctime(d->mtime));
-       ts[16] = 0;
-       now = time(0);
-       if(now - d->mtime > 6*30*24*60*60)
-               memmove(ts+11, ts+23, 5);
-       if(lflag){
-               /* Unix style long listing */
-               if(DMDIR&d->mode){
-                       links = 2;
-                       d->length = 512;
-               } else
-                       links = 1;
-
-               Bprint(b, "%s %3d %-8s %-8s %7lld %s ",
-                       mode2asc(d->mode), links,
-                       d->uid, d->gid, d->length, ts+4);
-       }
-       if(Cflag && maxnamelen < 40){
-               n = strlen(name);
-               pad = ((col+maxnamelen)/(maxnamelen+1))*(maxnamelen+1);
-               if(pad+maxnamelen+1 < 60){
-                       Bprint(b, "%*s", pad-col+n, name);
-                       col = pad+n;
-               }
-               else{
-                       Bprint(b, "\r\n%s", name);
-                       col = n;
-               }
-       }
-       else{
-               if(dname)
-                       Bprint(b, "%s/", dname);
-               Bprint(b, "%s\r\n", name);
-       }
-       free(d);
-}
-int
-dircomp(void *va, void *vb)
+void 
+listprint(Biobuf *data, Dir *d, char *dirname)
 {
-       int rv;
-       Dir *a, *b;
+       char *ts, *mode;
 
-       a = va;
-       b = vb;
-
-       if(tflag)
-               rv = b->mtime - a->mtime;
-       else
-               rv = strcmp(a->name, b->name);
-       return (rflag?-1:1)*rv;
-}
-void
-listdir(char *name, Biobufhdr *b, int lflag, int *printname, Globlist *gl)
-{
-       Dir *p;
-       int fd, n, i, l;
-       char *dname;
-       uvlong total;
+       ts = strdup(ctime(d->mtime));
+       ts[16] = '\0';
+       if(time(0) - d->mtime > 6 * 30 * 24 * 60 * 60)
+               memmove(ts + 11, ts + 23, 5);
 
-       col = 0;
+       mode = mode2asc(d->mode);
 
-       fd = open(name, OREAD);
-       if(fd < 0){
-               Bprint(b, "can't read %s: %r\r\n", name);
-               return;
-       }
-       dname = 0;
-       if(*printname){
-               if(Rflag || lflag)
-                       Bprint(b, "\r\n%s:\r\n", name);
-               else
-                       dname = name;
-       }
-       n = dirreadall(fd, &p);
-       close(fd);
-       if(Cflag){
-               for(i = 0; i < n; i++){
-                       l = strlen(p[i].name);
-                       if(l > maxnamelen)
-                               maxnamelen = l;
-               }
-       }
+       if(dirname)
+               reply(data, "%s %3d %-8s %-8s %7lld %s %s/%s", 
+                       mode, 1, d->uid, d->gid, d->length, ts + 4, dirname, d->name);
+       else
+               reply(data, "%s %3d %-8s %-8s %7lld %s %s",
+                       mode, 1, d->uid, d->gid, d->length, ts + 4, d->name);
 
-       /* Unix style total line */
-       if(lflag){
-               total = 0;
-               for(i = 0; i < n; i++){
-                       if(p[i].qid.type & QTDIR)
-                               total += 512;
-                       else
-                               total += p[i].length;
-               }
-               Bprint(b, "total %ulld\r\n", total/512);
-       }
+       free(mode);
+       free(ts);
+}
 
-       qsort(p, n, sizeof(Dir), dircomp);
-       for(i = 0; i < n; i++){
-               if(Rflag && (p[i].qid.type & QTDIR)){
-                       *printname = 1;
-                       globadd(gl, name, p[i].name);
-               }
-               listfile(b, p[i].name, lflag, dname);
-       }
-       free(p);
+int 
+listcmd(Ftpd *ftpd, char *arg)
+{
+       return list(ftpd, arg, listprint);
 }
-void
-list(char *arg, int lflag)
+
+int 
+loginuser(Ftpd *ftpd, char *pass, char *nsfile)
 {
-       Dir *d;
-       Globlist *gl;
-       Glob *g;
-       int dfd, printname;
-       int i, n, argc;
-       char *alist[Narg];
-       char **argv;
-       Biobufhdr bh;
-       uchar buf[512];
-       char *p, *s;
-
-       if(arg == 0)
-               arg = "";
-
-       if(debug)
-               logit("ls %s (. = %s)", arg, curdir);
-
-       /* process arguments, understand /bin/ls -l option */
-       argv = alist;
-       argv[0] = "/bin/ls";
-       argc = getfields(arg, argv+1, Narg-2, 1, " \t") + 1;
-       argv[argc] = 0;
-       rflag = 0;
-       tflag = 0;
-       Rflag = 0;
-       Cflag = 0;
-       col = 0;
-       ARGBEGIN{
-       case 'l':
-               lflag++;
-               break;
-       case 'R':
-               Rflag++;
-               break;
-       case 'C':
-               Cflag++;
-               break;
-       case 'r':
-               rflag++;
-               break;
-       case 't':
-               tflag++;
-               break;
-       }ARGEND;
-       if(Cflag)
-               lflag = 0;
+       char *user;
 
-       dfd = dialdata();
-       if(dfd < 0){
-               reply("425 Error opening data connection: %r");
-               return;
-       }
-       reply("150 Opened data connection (%s)", data);
+       user = ftpd->user.name;
 
-       Binits(&bh, dfd, OWRITE, buf, sizeof(buf));
-       if(argc == 0){
-               argc = 1;
-               argv = alist;
-               argv[0] = ".";
+       putenv("service", "ftp");
+       if(!ftpd->user.isnone) {
+               if(login(user, pass, nsfile) < 0)
+                       return reply(ftpd->out, "530 Not logged in: bad password");
+       } else {
+               if(newns(user, nsfile) < 0)
+                       return reply(ftpd->out, "530 Not logged in: user out of service");
        }
 
-       for(i = 0; i < argc; i++){
-               chdir(curdir);
-               gl = glob(argv[i]);
-               if(gl == nil)
-                       continue;
+       getwd(ftpd->user.cwd, Maxpath);
 
-               printname = gl->first != nil && gl->first->next != nil;
-               maxnamelen = 8;
-
-               if(Cflag)
-                       for(g = gl->first; g; g = g->next)
-                               if(g->glob && (n = strlen(s_to_c(g->glob))) > maxnamelen)
-                                       maxnamelen = n;
-               while(s = globiter(gl)){
-                       if(debug)
-                               logit("glob %s", s);
-                       p = abspath(s);
-                       if(p == nil){
-                               free(s);
-                               continue;
-                       }
-                       d = dirstat(p);
-                       if(d == nil){
-                               free(s);
-                               continue;
-                       }
-                       if(d->qid.type & QTDIR)
-                               listdir(s, &bh, lflag, &printname, gl);
-                       else
-                               listfile(&bh, s, lflag, 0);
-                       free(s);
-                       free(d);
-               }
-               globlistfree(gl);
-       }
-       if(Cflag)
-               Bprint(&bh, "\r\n");
-       Bflush(&bh);
-       close(dfd);
+       logit("login: %s in dir %s with ns %s",
+               ftpd->user.name,
+               ftpd->user.cwd,
+               nsfile);
 
-       reply("226 Transfer complete (list %s)", arg);
+       ftpd->user.loggedin = 1;
+       if(ftpd->user.isnone)
+               return reply(ftpd->out, "230 Logged in: anonymous access");
+       else
+               return reply(ftpd->out, "230 Logged in");
 }
-int
-namelistcmd(char *arg)
+
+void 
+nlistprint(Biobuf *data, Dir *d, char*)
 {
-       return asproc(list, arg, 0);
+       reply(data, "%s", d->name);
 }
-int
-listcmd(char *arg)
+
+int 
+nlistcmd(Ftpd *ftpd, char *arg)
 {
-       return asproc(list, arg, 1);
+       return list(ftpd, arg, nlistprint);
+}
+
+int 
+noopcmd(Ftpd *ftpd, char *arg)
+{
+       USED(arg);
+       return reply(ftpd->out, "200 Plan 9 FTP Server still alive");
 }
 
-/*
- * fuse compatability
- */
 int
-oksiteuser(void)
+mkdircmd(Ftpd *ftpd, char *arg)
 {
-       char buf[64];
-       int fd, n;
+       int fd;
+
+       if(!arg)
+               reply(ftpd->out, "501 Mkdir command requires argument.");
+       if(ftpd->user.isnone)
+               reply(ftpd->out, "550 Permission denied");
 
-       fd = open("#c/user", OREAD);
+       cleanname(arg);
+       fd = create(arg, OREAD, DMDIR|0755);
        if(fd < 0)
-               return 1;
-       n = read(fd, buf, sizeof buf - 1);
-       if(n > 0){
-               buf[n] = 0;
-               if(strcmp(buf, "none") == 0)
-                       n = -1;
-       }
+               return reply(ftpd->out, "550 Can't create %s: %r", arg);
        close(fd);
-       return n > 0;
+
+       return reply(ftpd->out, "226 %s created", arg);
 }
 
-int
-sitecmd(char *arg)
+void 
+mlsdprint(Biobuf *data, Dir *d, char*)
 {
-       char *f[4];
-       int nf, r;
-       Dir *d;
+       Tm mtime;
 
-       if(arg == 0)
-               return reply("501 bad site command");
-       nf = tokenize(arg, f, nelem(f));
-       if(nf != 3 || cistrcmp(f[0], "chmod") != 0)
-               return reply("501 bad site command");
-       if(!oksiteuser())
-               return reply("550 Permission denied");
-       d = dirstat(f[2]);
-       if(d == nil)
-               return reply("501 site chmod: file does not exist");
-       d->mode &= ~0777;
-       d->mode |= strtoul(f[1], 0, 8) & 0777;
-       r = dirwstat(f[2], d);
-       free(d);
-       if(r < 0)
-               return reply("550 Permission denied %r");
-       return reply("200 very well, then");
- }
-
-/*
- *  return the size of the file
- */
-int
-sizecmd(char *arg)
-{
-       Dir *d;
-       int rv;
+       tmtime(&mtime, d->mtime, nil);
+       reply(data, "Type=%s;Size=%d;Modify=%Ï„;Unix.groupname=%s;Unix.ownername=%s; %s", 
+               (d->mode & DMDIR ? "dir" : "file"), d->length, tmfmt(&mtime, "YYYYMMDDhhmmss"), 
+               d->gid, d->uid, d->name);
+}
 
-       if(arg == 0)
-               return reply("501 Size command requires pathname");
-       arg = abspath(arg);
-       d = dirstat(arg);
-       if(d == nil)
-               return reply("501 %r accessing %s", arg);
-       rv = reply("213 %lld", d->length);
-       free(d);
-       return rv;
+int 
+mlsdcmd(Ftpd *ftpd, char *arg)
+{
+       return list(ftpd, arg, mlsdprint);
 }
 
-/*
- *  return the modify time of the file
- */
-int
-mdtmcmd(char *arg)
+int 
+mlstcmd(Ftpd *ftpd, char *arg)
 {
        Dir *d;
-       Tm *t;
-       int rv;
+       char *path;
 
-       if(arg == 0)
-               return reply("501 Mdtm command requires pathname");
-       if(arg == 0)
-               return reply("550 Permission denied");
-       d = dirstat(arg);
-       if(d == nil)
-               return reply("501 %r accessing %s", arg);
-       t = gmtime(d->mtime);
-       rv = reply("213 %4.4d%2.2d%2.2d%2.2d%2.2d%2.2d",
-                       t->year+1900, t->mon+1, t->mday,
-                       t->hour, t->min, t->sec);
-       free(d);
-       return rv;
-}
+       if(arg != nil)
+               path = arg;
+       else
+               path = ftpd->user.cwd;
 
-/*
- *  set an offset to start reading a file from
- *  only lasts for one command
- */
-int
-restartcmd(char *arg)
-{
-       if(arg == 0)
-               return reply("501 Restart command requires offset");
-       offset = atoll(arg);
-       if(offset < 0){
-               offset = 0;
-               return reply("501 Bad offset");
-       }
+       d = dirstat(path);
+       if(!d)
+               return reply(ftpd->out, "500 Mlst failed: %r");
 
-       return reply("350 Restarting at %lld. Send STORE or RETRIEVE", offset);
+       reply(ftpd->out, "250-MLST %s", arg);
+       Bprint(ftpd->out, " ");
+       mlsdprint(ftpd->out, d, nil);
+       free(d);
+
+       return reply(ftpd->out, "250 End");
 }
 
-/*
- *  send a file to the user
- */
-int
-crlfwrite(int fd, char *p, int n)
+int 
+optscmd(Ftpd *ftpd, char *arg)
 {
-       char *ep, *np;
-       char buf[2*Nbuf];
+       if(cistrcmp(arg, "utf8 on") == 0)
+               return reply(ftpd->out, "200 UTF8 always on");
 
-       for(np = buf, ep = p + n; p < ep; p++){
-               if(*p == '\n')
-                       *np++ = '\r';
-               *np++ = *p;
-       }
-       if(write(fd, buf, np - buf) == np - buf)
-               return n;
-       else
-               return -1;
+       return reply(ftpd->out, "501 Option not implemented");
 }
-void
-retrievedir(char *arg)
-{
-       int n;
-       char *p;
-       String *file;
 
-       if(type != Timage){
-               reply("550 This file requires type binary/image");
-               return;
-       }
+int 
+passcmd(Ftpd *ftpd, char *arg)
+{
+       char *nsfile;
 
-       file = s_copy(arg);
-       p = strrchr(s_to_c(file), '/');
-       if(p != s_to_c(file)){
-               *p++ = 0;
-               chdir(s_to_c(file));
-       } else {
-               chdir("/");
-               p = s_to_c(file)+1;
-       }
+       if(strlen(ftpd->user.name) == 0)
+               return reply(ftpd->out, "531 Specify a user first");
 
-       n = transfer("/bin/tar", "c", p, 0, 1);
-       if(n < 0)
-               logit("get %s failed", arg);
+       nsfile = smprint("/usr/%s/lib/namespace.ftp", ftpd->user.name);
+       if(ftpd->user.isnone)
+               loginuser(ftpd, arg, namespace);
+       else if(access(nsfile, 0) == 0)
+               loginuser(ftpd, arg, nsfile);
        else
-               logit("get %s OK %d", arg, n);
-       s_free(file);
-}
-void
-retrieve(char *arg, int arg2)
-{
-       int dfd, fd, n, i, bytes;
-       Dir *d;
-       char buf[Nbuf];
-       char *p, *ep;
+               loginuser(ftpd, arg, "/lib/namespace");
+       free(nsfile);
 
-       USED(arg2);
+       return 0;
+}
 
-       p = strchr(arg, '\r');
-       if(p){
-               logit("cr in file name", arg);
-               *p = 0;
-       }
+int 
+pasvcmd(Ftpd *ftpd, char *arg)
+{
+       NetConnInfo *nci;
+       Passive *p;
 
-       fd = open(arg, OREAD);
-       if(fd == -1){
-               n = strlen(arg);
-               if(n > 4 && strcmp(arg+n-4, ".tar") == 0){
-                       *(arg+n-4) = 0;
-                       d = dirstat(arg);
-                       if(d != nil){
-                               if(d->qid.type & QTDIR){
-                                       retrievedir(arg);
-                                       free(d);
-                                       return;
-                               }
-                               free(d);
-                       }
-               }
-               logit("get %s failed", arg);
-               reply("550 Error opening %s: %r", arg);
-               return;
-       }
-       if(offset != 0)
-               if(seek(fd, offset, 0) < 0){
-                       reply("550 %s: seek to %lld failed", arg, offset);
-                       close(fd);
-                       return;
-               }
-       d = dirfstat(fd);
-       if(d != nil){
-               if(d->qid.type & QTDIR){
-                       reply("550 %s: not a plain file.", arg);
-                       close(fd);
-                       free(d);
-                       return;
-               }
-               free(d);
-       }
+       USED(arg);
 
-       n = read(fd, buf, sizeof(buf));
-       if(n < 0){
-               logit("get %s failed", arg, mailaddr, nci->rsys);
-               reply("550 Error reading %s: %r", arg);
-               close(fd);
-               return;
+       p = &ftpd->conn.pasv;
+       if(p->inuse) {
+               close(p->afd);
+               p->inuse = 0;
        }
 
-       if(type != Timage)
-               for(p = buf, ep = &buf[n]; p < ep; p++)
-                       if(*p & 0x80){
-                               close(fd);
-                               reply("550 This file requires type binary/image");
-                               return;
-                       }
+       if(mountnet(ftpd) < 0)
+               return 0;
 
-       reply("150 Opening data connection for %s (%s)", arg, data);
-       dfd = dialdata();
-       if(dfd < 0){
-               reply("425 Error opening data connection: %r");
-               close(fd);
-               return;
+       p->afd = announce("tcp!*!0", p->adir);
+       if(p->afd < 0) {
+               unmountnet();
+               return reply(ftpd->out, "500 No free ports");
        }
+       nci = getnetconninfo(p->adir, -1);
+       unmountnet();
 
-       bytes = 0;
-       do {
-               switch(type){
-               case Timage:
-                       i = write(dfd, buf, n);
-                       break;
-               default:
-                       i = crlfwrite(dfd, buf, n);
-                       break;
-               }
-               if(i != n){
-                       close(fd);
-                       close(dfd);
-                       logit("get %s %r to data connection after %d", arg, bytes);
-                       reply("550 Error writing to data connection: %r");
-                       return;
-               }
-               bytes += n;
-       } while((n = read(fd, buf, sizeof(buf))) > 0);
+       parseip(p->ipaddr, ftpd->conn.nci->lsys);
+       if(ipcmp(p->ipaddr, v4prefix) == 0 || ipcmp(p->ipaddr, IPnoaddr) == 0)
+               parseip(p->ipaddr, ftpd->conn.nci->lsys);
+       p->port = atoi(nci->lserv);
 
-       if(n < 0)
-               logit("get %s %r after %d", arg, bytes);
+       freenetconninfo(nci);
+       p->inuse = 1;
 
-       close(fd);
-       close(dfd);
-       reply("226 Transfer complete");
-       logit("get %s OK %d", arg, bytes);
+       dprint("dbg: pasv mode port %d", p->port);
+       return reply(ftpd->out, "227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)", 
+               p->ipaddr[IPv4off + 0], p->ipaddr[IPv4off + 1], 
+               p->ipaddr[IPv4off + 2], p->ipaddr[IPv4off + 3],
+               p->port >> 8, p->port & 0xff);
 }
-int
-retrievecmd(char *arg)
+
+int 
+pbszcmd(Ftpd *ftpd, char *arg)
 {
-       if(arg == 0)
-               return reply("501 Retrieve command requires an argument");
-       arg = abspath(arg);
-       if(arg == 0)
-               return reply("550 Permission denied");
+       USED(arg);
 
-       return asproc(retrieve, arg, 0);
+       /* tls is streaming and the only method we support */
+       return reply(ftpd->out, "200 Ok.");
 }
 
-/*
- *  get a file from the user
- */
-int
-lfwrite(int fd, char *p, int n)
+int 
+protcmd(Ftpd *ftpd, char *arg)
 {
-       char *ep, *np;
-       char buf[Nbuf];
+       if(!arg)
+               return reply(ftpd->out, "500 Prot command needs a level");
 
-       for(np = buf, ep = p + n; p < ep; p++){
-               if(*p != '\r')
-                       *np++ = *p;
+       switch(arg[0]) {
+       case 'p':
+       case 'P':
+               ftpd->conn.tlsondata = 1;
+               return reply(ftpd->out, "200 Protection level set");
+       case 'c':
+       case 'C':
+               ftpd->conn.tlsondata = 0;
+               return reply(ftpd->out, "200 Protection level set");
+       default:
+               return reply(ftpd->out, "504 Unknown protection level");
        }
-       if(write(fd, buf, np - buf) == np - buf)
-               return n;
-       else
-               return -1;
 }
-void
-store(char *arg, int fd)
-{
-       int dfd, n, i;
-       char buf[Nbuf];
-
-       reply("150 Opening data connection for %s (%s)", arg, data);
-       dfd = dialdata();
-       if(dfd < 0){
-               reply("425 Error opening data connection: %r");
-               close(fd);
-               return;
-       }
 
-       while((n = read(dfd, buf, sizeof(buf))) > 0){
-               switch(type){
-               case Timage:
-                       i = write(fd, buf, n);
-                       break;
-               default:
-                       i = lfwrite(fd, buf, n);
-                       break;
-               }
-               if(i != n){
-                       close(fd);
-                       close(dfd);
-                       reply("550 Error writing file");
-                       return;
-               }
-       }
-       close(fd);
-       close(dfd);
-       logit("put %s OK", arg);
-       reply("226 Transfer complete");
+int 
+portcmd(Ftpd *ftpd, char *arg)
+{
+       char *field[7];
+       char data[64];
+
+       if(!arg)
+               return reply(ftpd->out, "501 Port command needs arguments");
+       if(getfields(arg, field, 7, 0, ", ") != 6)
+               return reply(ftpd->out, "501 Incorrect port specification");
+       
+       snprint(data, sizeof(data), "tcp!%.3s.%.3s.%.3s.%.3s!%d", 
+                       field[0], field[1], field[2], field[3], 
+                       atoi(field[4]) * 256 + atoi(field[5]));
+       strncpy(ftpd->conn.data, data, sizeof(ftpd->conn.data));
+
+       return reply(ftpd->out, "200 Data port is %s", data);
 }
+
 int
-storecmd(char *arg)
+pwdcmd(Ftpd *ftpd, char *arg)
 {
-       int fd, rv;
-
-       if(arg == 0)
-               return reply("501 Store command requires an argument");
-       arg = abspath(arg);
-       if(arg == 0)
-               return reply("550 Permission denied");
-       if(isnone && strncmp(arg, "/incoming/", sizeof("/incoming/")-1))
-               return reply("550 Permission denied");
-       if(offset){
-               fd = open(arg, OWRITE);
-               if(fd == -1)
-                       return reply("550 Error opening %s: %r", arg);
-               if(seek(fd, offset, 0) == -1)
-                       return reply("550 Error seeking %s to %d: %r",
-                               arg, offset);
-       } else {
-               fd = create(arg, OWRITE, createperm);
-               if(fd == -1)
-                       return reply("550 Error creating %s: %r", arg);
-       }
-
-       rv = asproc(store, arg, fd);
-       close(fd);
-       return rv;
+       USED(arg);
+       return reply(ftpd->out, "257 \"%s\" is the current directory", ftpd->user.cwd);
 }
-int
-appendcmd(char *arg)
+
+int 
+quitcmd(Ftpd *ftpd, char *arg)
 {
-       int fd, rv;
-
-       if(arg == 0)
-               return reply("501 Append command requires an argument");
-       if(isnone)
-               return reply("550 Permission denied");
-       arg = abspath(arg);
-       if(arg == 0)
-               return reply("550 Error creating %s: Permission denied", arg);
-       fd = open(arg, OWRITE);
-       if(fd == -1){
-               fd = create(arg, OWRITE, createperm);
-               if(fd == -1)
-                       return reply("550 Error creating %s: %r", arg);
-       }
-       seek(fd, 0, 2);
+       USED(arg);
 
-       rv = asproc(store, arg, fd);
-       close(fd);
-       return rv;
+       if(ftpd->user.loggedin)
+               logit("quit: %s", ftpd->user.name);
+
+       reply(ftpd->out, "200 Goodbye.");
+       return -1;
 }
-int
-storeucmd(char *arg)
+
+int 
+resetcmd(Ftpd *ftpd, char *arg)
 {
-       int fd, rv;
-       char name[Maxpath];
+       if(!arg)
+               return reply(ftpd->out, "501 Restart command requires offset");
+       ftpd->offset = atoll(arg);
+       if(ftpd->offset < 0) {
+               ftpd->offset = 0;
+               return reply(ftpd->out, "501 Bad offset");
+       }
 
-       USED(arg);
-       if(isnone)
-               return reply("550 Permission denied");
-       strncpy(name, "ftpXXXXXXXXXXX", sizeof name);
-       mktemp(name);
-       fd = create(name, OWRITE, createperm);
-       if(fd == -1)
-               return reply("550 Error creating %s: %r", name);
-
-       rv = asproc(store, name, fd);
-       close(fd);
-       return rv;
+       return reply(ftpd->out, "350 Restarting at %lld");
 }
 
-int
-mkdircmd(char *name)
+int 
+retreivecmd(Ftpd *ftpd, char *arg)
 {
-       int fd;
+       Dir *d;
+       Biobuf *fd, *data;
+       char *line;
+       char buf[4096];
+       long rsz;
 
-       if(name == 0)
-               return reply("501 Mkdir command requires an argument");
-       if(isnone)
-               return reply("550 Permission denied");
-       name = abspath(name);
-       if(name == 0)
-               return reply("550 Permission denied");
-       fd = create(name, OREAD, DMDIR|0775);
-       if(fd < 0)
-               return reply("550 Can't create %s: %r", name);
-       close(fd);
-       return reply("226 %s created", name);
+       d = dirstat(arg);
+       if(!d)
+               return reply(ftpd->out, "550 Error opening %s: %r", arg);
+       if(d->mode & DMDIR)
+               return reply(ftpd->out, "550 %s is a directory", arg);
+       free(d);
+
+       fd = Bopen(arg, OREAD);
+       if(!fd)
+               return reply(ftpd->out, "550 Error opening %s: %r", arg);
+
+       if(ftpd->offset != 0)
+               Bseek(fd, ftpd->offset, 0);
+
+       data = dialdata(ftpd, 0);
+       if(ftpd->type == Tascii)
+               while(line = Brdstr(fd, '\n', 1))
+                       reply(data, line);
+       else
+               while(rsz = Bread(fd, buf, sizeof(buf)))
+                       if(rsz > 0)
+                               Bwrite(data, buf, rsz);
+       closedata(ftpd, data, 0);
+
+       logit("retreive: user %s file %s", ftpd->user.name, arg);
+
+       return 0;
 }
 
 int
-delcmd(char *name)
+renamefromcmd(Ftpd *ftpd, char *arg)
 {
-       if(name == 0)
-               return reply("501 Rmdir/delete command requires an argument");
-       if(isnone)
-               return reply("550 Permission denied");
-       name = abspath(name);
-       if(name == 0)
-               return reply("550 Permission denied");
-       if(remove(name) < 0)
-               return reply("550 Can't remove %s: %r", name);
-       else
-               return reply("226 %s removed", name);
+       if(!arg)
+               return reply(ftpd->out, "501 Rename command requires an argument");
+       if(ftpd->user.isnone)
+               return reply(ftpd->out, "550 Permission denied");
+       
+       cleanname(arg);
+       ftpd->renamefrom = strdup(arg);
+
+       return reply(ftpd->out, "350 Rename %s to...", arg);    
 }
 
-/*
- *  kill off the last transfer (if the process still exists)
- */
 int
-abortcmd(char *arg)
+renametocmd(Ftpd *ftpd, char *arg)
 {
-       USED(arg);
+       Dir *from, *to, nd;
 
-       logit("abort pid %d", pid);
-       if(pid){
-               if(postnote(PNPROC, pid, "kill") == 0)
-                       reply("426 Command aborted");
-               else
-                       logit("postnote pid %d %r", pid);
+       if(!arg)
+               return reply(ftpd->out, "501 Rename command requires an argument");
+       if(ftpd->user.isnone)
+               return reply(ftpd->out, "550 Permission denied");
+       if(!ftpd->renamefrom)
+               return reply(ftpd->out, "550 Rnto must be preceded by rnfr");
+
+       from = dirstat(ftpd->renamefrom);
+       if(!from) {
+               free(from);
+               return reply(ftpd->out, "550 Can't stat %s", ftpd->renamefrom);
        }
-       return reply("226 Abort processed");
+
+       to = dirstat(arg);
+       if(to) {
+               free(from); free(to);
+               return reply(ftpd->out, "550 Can't rename: target %s exists", arg);
+       }
+
+       nulldir(&nd);
+       nd.name = arg;
+       if(dirwstat(ftpd->renamefrom, &nd) < 0)
+               reply(ftpd->out, "550 Can't rename %s to %s: %r", ftpd->renamefrom, arg);
+       else
+               reply(ftpd->out, "250 %s now %s", ftpd->renamefrom, arg);
+       
+       free(ftpd->renamefrom);
+       ftpd->renamefrom = nil;
+       free(from);
+
+       return 0;
 }
 
-int
-systemcmd(char *arg)
+int 
+systemcmd(Ftpd *ftpd, char *arg)
 {
        USED(arg);
-       return reply("215 UNIX Type: L8 Version: Plan 9");
+       reply(ftpd->out, "215 UNIX Type: L8 Version: Plan 9");
+       return 0;
 }
 
 int
-helpcmd(char *arg)
+storecmd(Ftpd *ftpd, char *arg)
 {
-       int i;
-       char buf[80];
-       char *p, *e;
+       int fd;
+       Biobuf *stored, *data;
+       char *line;
+       char buf[4096];
+       long rsz;
 
-       USED(arg);
-       reply("214- the following commands are implemented:");
-       buf[0] = 0;
-       p = buf;
-       e = buf+sizeof buf;
-       for(i = 0; cmdtab[i].name; i++){
-               if((i%8) == 0){
-                       reply("214-%s", buf);
-                       p = buf;
-               }
-               p = seprint(p, e, " %-5.5s", cmdtab[i].name);
+       if(!arg)
+               return reply(ftpd->out, "501 Store command needs an argument");
+
+       arg = cleanname(arg);
+       if(ftpd->offset){
+               fd = open(arg, OWRITE);
+               if(fd < 0)
+                       return reply(ftpd->out, "550 Error opening %s: %r", arg);
+               if(seek(fd, ftpd->offset, 0) < 0)
+                       return reply(ftpd->out, "550 Error seeking in %s to %d: %r", arg, ftpd->offset);
+       } else {
+               fd = create(arg, OWRITE, 0660);
+               if(fd < 0)
+                       return reply(ftpd->out, "550 Error creating %s: %r", arg);
        }
-       if(p != buf)
-               reply("214-%s", buf);
-       reply("214 ");
-       return 0;
-}
 
-/*
- *  renaming a file takes two commands
- */
-static String *filepath;
+       stored = Bfdopen(fd, OWRITE);
+       data = dialdata(ftpd, 1);
 
-int
-rnfrcmd(char *from)
-{
-       if(isnone)
-               return reply("550 Permission denied");
-       if(from == 0)
-               return reply("501 Rename command requires an argument");
-       from = abspath(from);
-       if(from == 0)
-               return reply("550 Permission denied");
-       if(filepath == nil)
-               filepath = s_copy(from);
-       else{
-               s_reset(filepath);
-               s_append(filepath, from);
+       if(ftpd->type == Tascii)
+               while(line = Brdstr(data, '\n', 1)) {
+                       if(line[Blinelen(data)] == '\r')
+                               line[Blinelen(data)] = '\0';
+                       Bprint(stored, "%s\n", line);
+       } else {
+               while((rsz = Bread(data, buf, sizeof(buf))) > 0)
+                               Bwrite(stored, buf, rsz);
        }
-       return reply("350 Rename %s to ...", s_to_c(filepath));
-}
-int
-rntocmd(char *to)
-{
-       int r;
-       Dir nd;
-       char *fp, *tp;
-
-       if(isnone)
-               return reply("550 Permission denied");
-       if(to == 0)
-               return reply("501 Rename command requires an argument");
-       to = abspath(to);
-       if(to == 0)
-               return reply("550 Permission denied");
-       if(filepath == nil || *(s_to_c(filepath)) == 0)
-               return reply("503 Rnto must be preceeded by an rnfr");
-
-       tp = strrchr(to, '/');
-       fp = strrchr(s_to_c(filepath), '/');
-       if((tp && fp == 0) || (fp && tp == 0)
-       || (fp && tp && (fp-s_to_c(filepath) != tp-to || memcmp(s_to_c(filepath), to, tp-to))))
-               return reply("550 Rename can't change directory");
-       if(tp)
-               to = tp+1;
 
-       nulldir(&nd);
-       nd.name = to;
-       if(dirwstat(s_to_c(filepath), &nd) < 0)
-               r = reply("550 Can't rename %s to %s: %r\n", s_to_c(filepath), to);
-       else
-               r = reply("250 %s now %s", s_to_c(filepath), to);
-       s_reset(filepath);
+       Bterm(stored);
+       closedata(ftpd, data, 0);
 
-       return r;
+       logit("store: user %s file %s", ftpd->user.name, arg);
+
+       return 0;
 }
 
-/*
- *  to dial out we need the network file system in our
- *  name space.
- */
 int
-dialdata(void)
+typecmd(Ftpd *ftpd, char *arg)
 {
-       int fd, cfd;
-       char ldir[40];
-       char err[ERRMAX];
+       int c;
+       char *x;
 
-       if(mountnet() < 0)
-               return -1;
+       if(!arg)
+               return reply(ftpd->out, "501 Type command needs an argument");
 
-       if(!passive.inuse)
-               fd = dial(data, "20", 0, 0);
-       else {
-               fd = -1;
-               alarm(5*60*1000);
-               cfd = listen(passive.adir, ldir);
-               alarm(0);
-               if(cfd >= 0){
-                       fd = accept(cfd, ldir);
-                       close(cfd);
+       x = arg;
+       while(c = *x++) {
+               switch(tolower(c)) {
+               case 'a':
+                       ftpd->type = Tascii;
+                       break;
+               case 'i':
+               case 'l':
+                       ftpd->type = Timage;
+                       break;
+               case '8':
+               case ' ':
+               case 'n':
+               case 't':
+               case 'c':
+                       break;
+               default:
+                       return reply(ftpd->out, "501 Unimplemented type %s", arg);
                }
        }
-       err[0] = 0;
-       errstr(err, sizeof err);
-       if(fd < 0)
-               logit("can't dial %s: %s", data, err);
-       unmountnet();
-       errstr(err, sizeof err);
-       return fd;
+
+       return reply(ftpd->out, "200 Type %s", (ftpd->type == Tascii ? "Ascii" : "Image"));
 }
 
 int
-postnote(int group, int pid, char *note)
+usercmd(Ftpd *ftpd, char *arg)
 {
-       char file[128];
-       int f, r;
-
-       /*
-        * Use #p because /proc may not be in the namespace.
-        */
-       switch(group) {
-       case PNPROC:
-               sprint(file, "#p/%d/note", pid);
-               break;
-       case PNGROUP:
-               sprint(file, "#p/%d/notepg", pid);
-               break;
-       default:
-               return -1;
-       }
+       if(ftpd->user.loggedin)
+               return reply(ftpd->out, "530 Already logged in as %s", ftpd->user.name);
 
-       f = open(file, OWRITE);
-       if(f < 0)
-               return -1;
+       if(arg == nil)
+               return reply(ftpd->out, "530 User command needs username");
 
-       r = strlen(note);
-       if(write(f, note, r) != r) {
-               close(f);
-               return -1;
+       if(anonall)
+               ftpd->user.isnone = 1;
+
+       if(strcmp(arg, "anonymous") == 0 || strcmp(arg, "ftp") == 0 || strcmp(arg, "none") == 0) {
+               if(!anonok && !anononly)
+                       return reply(ftpd->out, "530 Not logged in: anonymous access disabled");
+
+               ftpd->user.isnone = 1;
+               strncpy(ftpd->user.name, "none", Maxpath);
+               return loginuser(ftpd, nil, namespace);
+       } else if(anononly) {
+               return reply(ftpd->out, "530 Not logged in: anonymous access only");
        }
-       close(f);
-       return 0;
+
+       strncpy(ftpd->user.name, arg, Maxpath);
+       return reply(ftpd->out, "331 Need password");
 }
 
-/*
- *  to circumscribe the accessible files we have to eliminate ..'s
- *  and resolve all names from the root.  We also remove any /bin/rc
- *  special characters to avoid later problems with executed commands.
- */
-char *special = "`;| ";
+Cmd cmdtab[] = {
+       /* cmd, fn, needlogin, needtls, asproc*/
+       {"abor",        abortcmd,               0,      0,      0},
+       {"allo",        noopcmd,                0,      0,      0},
+       {"auth",        authcmd,                0,      0,      0},
+       {"cwd",         cwdcmd,                 1,      0,      0},
+       {"dele",        deletecmd,              1,      0,      0},
+       {"feat",        featcmd,                0,      0,      0},
+       {"list",        listcmd,                1,      0,      1},
+       {"nlst",        nlistcmd,               1,      0,      1},
+       {"noop",        noopcmd,                0,      0,      0},
+       {"mkd",         mkdircmd,               1,      0,      0},
+       {"mlsd",        mlsdcmd,                1,      0,      0},
+       {"mlst",        mlstcmd,                1,      0,      1},
+       {"opts",        optscmd,                0,      0,      0},
+       {"pass",        passcmd,                0,      1,      0},
+       {"pasv",        pasvcmd,                0,      0,      0},
+       {"pbsz",        pbszcmd,                0,      1,      0},
+       {"prot",        protcmd,                0,      1,      0},
+       {"port",        portcmd,                0,      0,      0},
+       {"pwd",         pwdcmd,                 0,      0,      0},
+       {"quit",        quitcmd,                0,      0,      0},
+       {"rest",        resetcmd,               0,      0,      0},
+       {"retr",        retreivecmd,    1,      0,      1},
+       {"rmd",         deletecmd,              1,      0,      0},
+       {"rnfr",        renamefromcmd,  1,      0,      0},
+       {"rnto",        renametocmd,    1,      0,      0},
+       {"syst",        systemcmd,              0,      0,      0},
+       {"stor",        storecmd,               1,      0,      1},
+       {"type",        typecmd,                0,      0,      0},
+       {"user",        usercmd,                0,      0,      0},
+       {nil,           nil,                    0,      0,      0},
+};
 
-char*
-abspath(char *origpath)
+void 
+usage(void)
 {
-       char *p, *sp, *path;
-       static String *rpath;
+       fprint(2, "usage: %s [-aAdei] [-c cert-path] [-n namespace-file]\n", argv0);
+       exits("usage");
+}
 
-       if(rpath == nil)
-               rpath = s_new();
-       else
-               s_reset(rpath);
-
-       if(origpath == nil)
-               s_append(rpath, curdir);
-       else{
-               if(*origpath != '/'){
-                       s_append(rpath, curdir);
-                       s_append(rpath, "/");
-               }
-               s_append(rpath, origpath);
-       }
-       path = s_to_c(rpath);
+void 
+main(int argc, char **argv)
+{
+       Ftpd ftpd;
+       char *cmd, *arg;
+       Cmd *t;
 
-       for(sp = special; *sp; sp++){
-               p = strchr(path, *sp);
-               if(p)
-                       *p = 0;
-       }
+       ARGBEGIN {
+       case 'a':
+               anonok = 1;
+               break;
+       case 'A':
+               anononly = 1;
+               break;
+       case 'c':
+               certpath = EARGF(usage());
+               break;
+       case 'd':
+               debug = 1;
+               break;
+       case 'e':
+               anonall = 1;
+               break;
+       case 'i':
+               implicittls = 1;
+               break;
+       case 'n':
+               namespace = EARGF(usage());
+               break;
+       default:
+               usage();
+       } ARGEND
 
-       cleanname(s_to_c(rpath));
-       rpath->ptr = rpath->base+strlen(rpath->base);
+       tmfmtinstall();
 
-       if(!accessok(s_to_c(rpath)))
-               return nil;
+       if(argc < 1)
+               ftpd.conn.nci = getnetconninfo(nil, 0);
+       else
+               ftpd.conn.nci = getnetconninfo(argv[argc - 1], 0);
+       if(!ftpd.conn.nci)
+               sysfatal("ftpd needs a network address");
 
-       return s_to_c(rpath);
-}
+       ftpd.in = mallocz(sizeof(Biobuf), 1);
+       ftpd.out = mallocz(sizeof(Biobuf), 1);
+       Binit(ftpd.in, 0, OREAD);
+       Binit(ftpd.out, 1, OWRITE);
 
-typedef struct Path Path;
-struct Path {
-       Path    *next;
-       String  *path;
-       int     inuse;
-       int     ok;
-};
+       /* open logfile */
+       syslog(0, "ftp", nil);
 
-enum
-{
-       Maxlevel = 16,
-       Maxperlevel= 8,
-};
+       if(certpath) {
+               ftpd.conn.cert = readcert(certpath, &ftpd.conn.certlen);
+               ftpd.conn.tls = mallocz(sizeof(TLSconn), 1);
 
-Path *pathlevel[Maxlevel];
+               /* we need a copy in case of namespace changes 
+                * NOTE: the default namespace needs to leave access to the tls device
+                * or anonymous logins with tls will be broken. */
+               ftpd.conn.tls->cert = malloc(ftpd.conn.certlen);
+               memcpy(ftpd.conn.tls->cert, ftpd.conn.cert, ftpd.conn.certlen);
+               ftpd.conn.tls->certlen = ftpd.conn.certlen;
 
-Path*
-unlinkpath(char *path, int level)
-{
-       String *s;
-       Path **l, *p;
-       int n;
-
-       n = 0;
-       for(l = &pathlevel[level]; *l; l = &(*l)->next){
-               p = *l;
-               /* hit */
-               if(strcmp(s_to_c(p->path), path) == 0){
-                       *l = p->next;
-                       p->next = nil;
-                       return p;
-               }
-               /* reuse */
-               if(++n >= Maxperlevel){
-                       *l = p->next;
-                       s = p->path;
-                       s_reset(p->path);
-                       memset(p, 0, sizeof *p);
-                       p->path = s_append(s, path);
-                       return p;
+               if(implicittls) {
+                       dprint("dbg: implicit tls mode");
+                       starttls(&ftpd);
                }
        }
 
-       /* allocate */
-       p = mallocz(sizeof *p, 1);
-       p->path = s_copy(path);
-       return p;
-}
+       reply(ftpd.out, "220 Plan 9 FTP server ready.");
+       alarm(Maxwait);
+       while(cmd = Brdstr(ftpd.in, '\n', 1)) {
+               alarm(0);
 
-void
-linkpath(Path *p, int level)
-{
-       p->next = pathlevel[level];
-       pathlevel[level] = p;
-       p->inuse = 1;
-}
+               /* strip cr */
+               char *p = strrchr(cmd, '\r');
+               if(p)
+                       *p = '\0';
 
-void
-addpath(Path *p, int level, int ok)
-{
-       p->ok = ok;
-       p->next = pathlevel[level];
-       pathlevel[level] = p;
-}
+               /* strip telnet control sequences */
+               while(*cmd && (uchar)*cmd == 255) {
+                       cmd++;
+                       if(*cmd)
+                               cmd++;
+               }
 
-int
-_accessok(String *s, int level)
-{
-       Path *p;
-       char *cp;
-       int lvl, offset;
-       static char httplogin[] = "/.httplogin";
-
-       if(level < 0)
-               return 1;
-       lvl = level;
-       if(lvl >= Maxlevel)
-               lvl = Maxlevel - 1;
-
-       p = unlinkpath(s_to_c(s), lvl);
-       if(p->inuse){
-               /* move to front */
-               linkpath(p, lvl);
-               return p->ok;
-       }
-       cp = strrchr(s_to_c(s), '/');
-       if(cp == nil)
-               offset = 0;
-       else
-               offset = cp - s_to_c(s);
-       s_append(s, httplogin);
-       if(access(s_to_c(s), AEXIST) == 0){
-               addpath(p, lvl, 0);
-               return 0;
-       }
+               /* get the arguments */
+               arg = strchr(cmd, ' ');
+               if(arg) {
+                       *arg++ = '\0';
+                       while(*arg == ' ')
+                               arg++;
+                       /* some clients always send a space */
+                       if(*arg == '\0')
+                               arg = nil;
+               }
 
-       /*
-        * There's no way to shorten a String without
-        * knowing the implementation.
-        */
-       s->ptr = s->base+offset;
-       s_terminate(s);
-       addpath(p, lvl, _accessok(s, level-1));
+               /* find the cmd and execute it */
+               if(*cmd == '\0')
+                       continue;
 
-       return p->ok;
-}
+               for(t = cmdtab; t->name; t++)
+                       if(cistrcmp(cmd, t->name) == 0) {
+                               if(t->needlogin && !ftpd.user.loggedin) {
+                                       reply(ftpd.out, "530 Command requires login");
+                               } else if(t->needtls && !ftpd.conn.tlson) {
+                                       reply(ftpd.out, "534 Command requires tls");
+                               } else {
+                                       if(t->fn != passcmd)
+                                               dprint("cmd: %s %s", cmd, arg);
+                                       if(t->asproc) {
+                                               dprint("cmd %s spawned as proc");
+                                               asproc(&ftpd, *t->fn, arg);
+                                       } else if((*t->fn)(&ftpd, arg) < 0)
+                                               goto exit;
+                               }
+                               break;
+                       }
 
-/*
- * check for a subdirectory containing .httplogin
- * at each level of the path.
- */
-int
-accessok(char *path)
-{
-       int level, r;
-       char *p;
-       String *npath;
+               /* reset the offset unless we just set it */
+               if(t->fn != resetcmd)
+                       ftpd.offset = 0;
+               if(!t->name)
+                       reply(ftpd.out, "502 %s command not implemented", cmd);
 
-       npath = s_copy(path);
-       p = s_to_c(npath)+1;
-       for(level = 1; level < Maxlevel; level++){
-               p = strchr(p, '/');
-               if(p == nil)
-                       break;
-               p++;
+               free(cmd);
+               alarm(Maxwait);
        }
 
-       r = _accessok(npath, level-1);
-       s_free(npath);
-
-       return r;
+exit:
+       free(ftpd.conn.tls);
+       freenetconninfo(ftpd.conn.nci);
+       Bterm(ftpd.in);
+       Bterm(ftpd.out);
+       free(ftpd.in);
+       free(ftpd.out);
+       exits(nil);
 }