code: plan9front

Download patch

ref: e72da62915b09d5673b0c0179ba8dfe045aeb8c3
parent: 9633c9fc65a833747a24cbd51f922afcf2859efd
author: foura <james@biobuf.link>
date: Sun May 2 11:29:43 EDT 2021

ip/ftpd: Add explict and implicit FTPS support.

Removed:
- Challenge reponse auth.
- Noworld login.
- Anonymous users writing files to /incoming.

--- a/sys/man/8/ipserv
+++ b/sys/man/8/ipserv
@@ -12,9 +12,11 @@
 .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 @@
 .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 @@
 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,22 +155,17 @@
 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
--- a/sys/src/cmd/ip/ftpd.c
+++ b/sys/src/cmd/ip/ftpd.c
@@ -1,893 +1,440 @@
 #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,
+enum {
+	Tascii,
+	Timage,
 
-	/* representation types */
-	Tascii=		0,
-	Timage=		1,
+	Maxpath = 512,
+	Maxwait = 1000 * 60 * 30, /* 30 minutes */
+};
 
-	/* transmission modes */
-	Mstream=	0,
-	Mblock=		1,
-	Mpage=		2,
+typedef struct Passive Passive;
+typedef struct Ftpd Ftpd;
+typedef struct Cmd Cmd;
 
-	/* file structure */
-	Sfile=		0,
-	Sblock=		1,
-	Scompressed=	2,
-
-	/* 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,
+struct Passive {
+	int inuse;
+	char adir[40];
+	int afd;
+	int port;
+	uchar ipaddr[IPaddrlen];
 };
 
-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*);
+struct Ftpd {
+	Biobuf *in, *out;
 
-int	dialdata(void);
-char*	abspath(char*);
-int	crlfwrite(int, char*, int);
-int	sodoff(void);
-int	accessok(char*);
+	struct conn {
+		int tlson, tlsondata;
+		NetConnInfo *nci;
+		TLSconn *tls;
+		uchar *cert;
+		int certlen;
+		char data[64];
+		Passive pasv;
+	} conn;
 
-typedef struct Cmd	Cmd;
-struct Cmd
-{
-	char	*name;
-	int	(*f)(char*);
-	int	needlogin;
+	struct user {
+		char cwd[Maxpath];
+		char name[Maxpath];
+		int loggedin;
+		int isnone;
+	} user;
+
+	int type;
+	vlong offset;
+	int cmdpid;
+	char *renamefrom;
 };
 
-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 Cmd {
+	char *name;
+	int (*fn)(Ftpd *, char *);
+	int needlogin;
+	int needtls;
+	int asproc;
 };
 
-#define NONENS "/lib/namespace.ftp"	/* default ns for none */
+char *certpath;
+char *namespace = "/lib/namespace.ftp";
+int implicittls;
+int debug;
+int anonok;
+int anononly;
+int anonall;
 
-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 */
+void 
+dprint(char *fmt, ...)
+{
+	char *msg;
+	va_list arg;
 
-ulong id;
+	if(!debug) return;
 
-typedef struct Passive Passive;
-struct Passive
-{
-	int	inuse;
-	char	adir[40];
-	int	afd;
-	int	port;
-	uchar	ipaddr[IPaddrlen];
-} passive;
+	va_start(arg, fmt);
+	msg = vsmprint(fmt, arg);
+	va_end(arg);
 
-#define FTPLOG "ftp"
+	syslog(0, "ftp", msg);
+	free(msg);
+}
 
-void
+void 
 logit(char *fmt, ...)
 {
-	char buf[8192];
+	char *msg;
 	va_list arg;
 
 	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)
+int 
+reply(Biobuf *bio, 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++;
-		}
-
-		/*
-		 *  ignore blank commands
-		 */
-		if(*cmd == 0)
-			continue;
-
-		/*
-		 *  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");
-}
-
-/*
- *  reply to a command
- */
-int
-reply(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);
+	Bwrite(bio, buf, s - buf);
+	Bflush(bio);
+
 	return 0;
 }
 
-int
-sodoff(void)
+void
+asproc(Ftpd *ftpd, int (*f)(Ftpd *, char *), char *arg)
 {
-	return reply("530 Sod off, service requires login");
-}
-
-/*
- *  run a command in a separate process
- */
-int
-asproc(void (*f)(char*, int), char *arg, int arg2)
-{
 	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;
 	}
-	return 0;
 }
 
-/*
- * run a command to filter a tail
- */
-int
-transfer(char *cmd, char *a1, char *a2, char *a3, int image)
+int 
+mountnet(Ftpd *ftpd)
 {
-	int n, dfd, fd, bytes, eofs, pid;
-	int pfd[2];
-	char buf[Nbuf], *p;
-	Waitmsg *w;
-
-	reply("150 Opening data connection for %s (%s)", cmd, data);
-	dfd = dialdata();
-	if(dfd < 0)
-		return reply("425 Error opening data connection: %r");
-
-	if(pipe(pfd) < 0)
-		return reply("520 Internal Error: %r");
-
-	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;
-		}
-		close(pfd[1]);
-		close(dfd);
-		break;
+	if(bind("#/", "/", MAFTER) == -1) {
+		reply(ftpd->out, "500 can't bind #/ to /: %r");
+		return -1;
 	}
 
-	/* wait for this command to finish */
-	for(;;){
-		w = wait();
-		if(w == nil || w->pid == pid)
-			break;
-		free(w);
+	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;
 	}
-	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);
-	}
-	free(w);
-	reply("226 Transfer complete");
-	return bytes;
-}
 
-int
-optscmd(char *arg)
-{
-	char *p;
-
-	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;
-	}
-	reply("502 %s option not implemented", arg);
 	return 0;
 }
 
-/*
- *  just reply OK
- */
-int
-nopcmd(char *arg)
+void 
+unmountnet(void)
 {
-	USED(arg);
-	reply("510 Plan 9 FTP daemon still alive");
-	return 0;
+	unmount(nil, "/net");
+	unmount("#/", "/");
 }
 
-/*
- *  login as user
- */
-int
-loginuser(char *user, char *nsfile, int gotoslash)
+Biobuf *
+dialdata(Ftpd *ftpd, int read)
 {
-	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");
+	Biobuf *bio;
+	TLSconn *tls;
+	int fd, cfd;
+	char ldir[40];
+
+	if(mountnet(ftpd) < 0)
+		return nil;
+
+	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);
+		}
 	}
-	getwd(curdir, sizeof(curdir));
-	if(gotoslash){
-		chdir("/");
-		strcpy(curdir, "/");
+
+	if(fd < 0) {
+		reply(ftpd->out, "425 Error opening data connection");
+		unmountnet();
+		return nil;
 	}
-	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)
-{
-	static ulong pause;
+	reply(ftpd->out, "150 Opened data connection");
 
-	if (pause) {
-		sleep(pause);			/* deter guessers */
-		if (pause < (1UL << 20))
-			pause *= 2;
-	} else
-		pause = 1000;
-}
+	tls = nil;
+	if(ftpd->conn.tlsondata) {
+		dprint("dbg: using tls on data channel");
 
-/*
- *  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)
-{
-	slowdown();
+		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);
 
-	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(fd < 0) {
+			reply(ftpd->out, "425 TLS on data connection failed");
+			unmountnet();
+			return nil;
+		}
 
-	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);
+		dprint("dbg: tlsserver done");
 	}
-	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");
-	}
-	else if(anon_only)
-		return reply("530 Not logged in: anonymous access only");
 
-	isnoworld = noworld(name);
-	if(isnoworld)
-		return reply("331 OK");
+	unmountnet();
+	if(read)
+		bio = Bfdopen(fd, OREAD);
+	else
+		bio = Bfdopen(fd, OWRITE);
+	bio->aux = tls;
 
-	/* 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 bio;
 }
 
-/*
- *  get a password, set up user if it works.
- */
-int
-passcmd(char *response)
+void 
+closedata(Ftpd *ftpd, Biobuf *bio, int fail)
 {
-	char namefile[128];
-	AuthInfo *ai;
-	Dir nd;
+	TLSconn *conn;
 
-	if(response == nil)
-		response = "";
+	conn = bio->aux;
 
-	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);
-	}
+	Bflush(bio);
+	Bterm(bio);
+	if(!fail)
+		reply(ftpd->out, "226 Transfer complete");
 
-	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);
-	} 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);
+	if(conn) {
+		free(conn->cert);
+		free(conn);
 	}
 }
 
-/*
- *  print working directory
- */
-int
-pwdcmd(char *arg)
+int 
+starttls(Ftpd *ftpd)
 {
-	if(arg)
-		return reply("550 Pwd takes no argument");
-	return reply("257 \"%s\" is the current directory", curdir);
-}
+	int fd;
 
-/*
- *  chdir
- */
-int
-cwdcmd(char *dir)
-{
-	char *rp;
-	char buf[Maxpath];
+	fd = tlsServer(0, ftpd->conn.tls);
+	if(fd < 0)
+		return -1;
 
-	/* 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);
+	dup(fd, 0);
+	dup(fd, 1);
+	ftpd->conn.tlson = 1;
 
-	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);
+	return 0;
 }
 
-/*
- *  chdir ..
- */
 int
-cdupcmd(char *dp)
+abortcmd(Ftpd *ftpd, char *arg)
 {
-	USED(dp);
-	return cwdcmd("..");
-}
-
-int
-quitcmd(char *arg)
-{
 	USED(arg);
-	reply("200 Bye");
-	if(pid)
-		postnote(PNPROC, pid, "kill");
-	return -1;
+
+	if(ftpd->cmdpid){
+		if(postnote(PNPROC, ftpd->cmdpid, "kill") == 0)
+			reply(ftpd->out, "426 Command aborted");
+		else
+			logit("postnote pid %d %r", ftpd->cmdpid);
+	}
+	return reply(ftpd->out, "226 Abort processed");
 }
 
-int
-typecmd(char *arg)
+int 
+authcmd(Ftpd *ftpd, char *arg)
 {
-	int c;
-	char *x;
+	if((cistrcmp(arg, "TLS") == 0) || (cistrcmp(arg, "TLS-C") == 0) || (cistrcmp(arg, "SSL") == 0)) {
 
-	x = arg;
-	if(arg == 0)
-		return reply("501 Type command needs arguments");
+		if(!ftpd->conn.tls)
+			return reply(ftpd->out, "431 tls not enabled");
 
-	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);
-		}
+		reply(ftpd->out, "234 starting tls");
+		if(starttls(ftpd) < 0)
+			return reply(ftpd->out, "431 tls failed");
+	} else {
+		return reply(ftpd->out, "502 security method %s not understood", arg);
 	}
-	return reply("200 Type %s", type==Tascii ? "Ascii" : "Image");
+
+	return 0;
 }
 
-int
-modecmd(char *arg)
+int 
+cwdcmd(Ftpd *ftpd, char *arg)
 {
-	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++;
+	char buf[Maxpath];
+
+	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);
 	}
-	return reply("200 Stream mode");
+
+	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);
 }
 
-int
-structcmd(char *arg)
+int 
+deletecmd(Ftpd *ftpd, char *arg)
 {
-	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);
-		}
-	}
-	return reply("200 File structure");
+	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
-portcmd(char *arg)
+int 
+featcmd(Ftpd *ftpd, 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);
+	USED(arg);
+	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
-mountnet(void)
+int 
+dircmp(void *va, void *vb)
 {
-	int rv;
+	Dir *a, *b;
 
-	rv = 0;
+	a = va;
+	b = vb;
 
-	if(bind("#/", "/", MAFTER) == -1){
-		logit("can't bind #/ to /: %r");
-		return reply("500 can't bind #/ to /: %r");
-	}
-
-	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("#/", "/");
-	}
-
-	return rv;
+	return strcmp(a->name, b->name);
 }
 
 void
-unmountnet(void)
+listdir(Ftpd *ftpd, Biobuf *data, char *path, void (*fn)(Biobuf *, Dir *d, char *dirname))
 {
-	unmount(0, "/net");
-	unmount("#/", "/");
+	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
-pasvcmd(char *arg)
+list(Ftpd *ftpd, char *arg, void (*fn)(Biobuf *, Dir *d, char *dirname))
 {
-	NetConnInfo *nnci;
-	Passive *p;
+	Biobuf *data;
+	int argc, i;
+	char *argv[32];
+	Globlist *gl;
+	char *path;
+	Dir *d;
 
-	USED(arg);
-	p = &passive;
-
-	if(p->inuse){
-		close(p->afd);
-		p->inuse = 0;
+	if(arg) {
+		argc = getfields(arg, argv, sizeof(argv)-1, 1, " \t");
+	} else {
+		argc = 1;
+		argv[0] = ftpd->user.cwd;
 	}
 
-	if(mountnet() < 0)
-		return 0;
+	data = dialdata(ftpd, 0);
+	if(!data)
+		return reply(ftpd->out, "500 List failed: couldn't dial data");
 
-	p->afd = announce("tcp!*!0", passive.adir);
-	if(p->afd < 0){
-		unmountnet();
-		return reply("500 No free ports");
-	}
-	nnci = getnetconninfo(p->adir, -1);
-	unmountnet();
+	for(i=0;i<argc;i++) {
+		gl = glob(argv[i]);
+		if(!gl)
+			continue;
 
-	/* 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);
+		while(path = globiter(gl)) {
+			cleanname(path);
 
-	freenetconninfo(nnci);
-	p->inuse = 1;
+			logit("list: path %s user %s", path, ftpd->user.name);
 
-	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);
-}
+			d = dirstat(path);
+			if(d->mode & DMDIR)
+				listdir(ftpd, data, path, fn);
+			else
+				(*fn)(data, d, nil);
 
-enum
-{
-	Narg=32,
-};
-int Cflag, rflag, tflag, Rflag;
-int maxnamelen;
-int col;
+			free(d);
+		}
+	}
 
-char*
+	closedata(ftpd, data, 0);
+
+	return 0;
+}
+
+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 @@
 	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 @@
 		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;
+	ts = strdup(ctime(d->mtime));
+	ts[16] = '\0';
+	if(time(0) - d->mtime > 6 * 30 * 24 * 60 * 60)
+		memmove(ts + 11, ts + 23, 5);
 
-	if(tflag)
-		rv = b->mtime - a->mtime;
+	mode = mode2asc(d->mode);
+
+	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
-		rv = strcmp(a->name, b->name);
-	return (rflag?-1:1)*rv;
+		reply(data, "%s %3d %-8s %-8s %7lld %s %s",
+			mode, 1, d->uid, d->gid, d->length, ts + 4, d->name);
+
+	free(mode);
+	free(ts);
 }
-void
-listdir(char *name, Biobufhdr *b, int lflag, int *printname, Globlist *gl)
-{
-	Dir *p;
-	int fd, n, i, l;
-	char *dname;
-	uvlong total;
 
-	col = 0;
-
-	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;
-		}
-	}
-
-	/* 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);
-	}
-
-	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;
+	char *user;
 
-	if(arg == 0)
-		arg = "";
+	user = ftpd->user.name;
 
-	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;
-
-	dfd = dialdata();
-	if(dfd < 0){
-		reply("425 Error opening data connection: %r");
-		return;
+	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");
 	}
-	reply("150 Opened data connection (%s)", data);
 
-	Binits(&bh, dfd, OWRITE, buf, sizeof(buf));
-	if(argc == 0){
-		argc = 1;
-		argv = alist;
-		argv[0] = ".";
-	}
+	getwd(ftpd->user.cwd, Maxpath);
 
-	for(i = 0; i < argc; i++){
-		chdir(curdir);
-		gl = glob(argv[i]);
-		if(gl == nil)
-			continue;
+	logit("login: %s in dir %s with ns %s",
+		ftpd->user.name,
+		ftpd->user.cwd,
+		nsfile);
 
-		printname = gl->first != nil && gl->first->next != nil;
-		maxnamelen = 8;
+	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");
+}
 
-		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);
-
-	reply("226 Transfer complete (list %s)", arg);
+void 
+nlistprint(Biobuf *data, Dir *d, char*)
+{
+	reply(data, "%s", d->name);
 }
-int
-namelistcmd(char *arg)
+
+int 
+nlistcmd(Ftpd *ftpd, char *arg)
 {
-	return asproc(list, arg, 0);
+	return list(ftpd, arg, nlistprint);
 }
-int
-listcmd(char *arg)
+
+int 
+noopcmd(Ftpd *ftpd, char *arg)
 {
-	return asproc(list, arg, 1);
+	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;
 
-	fd = open("#c/user", OREAD);
+	if(!arg)
+		reply(ftpd->out, "501 Mkdir command requires argument.");
+	if(ftpd->user.isnone)
+		reply(ftpd->out, "550 Permission denied");
+
+	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");
- }
+	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);
+}
 
-/*
- *  return the size of the file
- */
-int
-sizecmd(char *arg)
+int 
+mlsdcmd(Ftpd *ftpd, char *arg)
 {
-	Dir *d;
-	int rv;
-
-	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;
+	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);
+	if(arg != nil)
+		path = arg;
+	else
+		path = ftpd->user.cwd;
+
+	d = dirstat(path);
+	if(!d)
+		return reply(ftpd->out, "500 Mlst failed: %r");
+
+	reply(ftpd->out, "250-MLST %s", arg);
+	Bprint(ftpd->out, " ");
+	mlsdprint(ftpd->out, d, nil);
 	free(d);
-	return rv;
+
+	return reply(ftpd->out, "250 End");
 }
 
-/*
- *  set an offset to start reading a file from
- *  only lasts for one command
- */
-int
-restartcmd(char *arg)
+int 
+optscmd(Ftpd *ftpd, 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");
-	}
+	if(cistrcmp(arg, "utf8 on") == 0)
+		return reply(ftpd->out, "200 UTF8 always on");
 
-	return reply("350 Restarting at %lld. Send STORE or RETRIEVE", offset);
+	return reply(ftpd->out, "501 Option not implemented");
 }
 
-/*
- *  send a file to the user
- */
-int
-crlfwrite(int fd, char *p, int n)
+int 
+passcmd(Ftpd *ftpd, char *arg)
 {
-	char *ep, *np;
-	char buf[2*Nbuf];
+	char *nsfile;
 
-	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;
-}
-void
-retrievedir(char *arg)
-{
-	int n;
-	char *p;
-	String *file;
+	if(strlen(ftpd->user.name) == 0)
+		return reply(ftpd->out, "531 Specify a user first");
 
-	if(type != Timage){
-		reply("550 This file requires type binary/image");
-		return;
-	}
-
-	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;
-	}
-
-	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);
+		loginuser(ftpd, arg, "/lib/namespace");
+	free(nsfile);
+
+	return 0;
 }
-void
-retrieve(char *arg, int arg2)
+
+int 
+pasvcmd(Ftpd *ftpd, char *arg)
 {
-	int dfd, fd, n, i, bytes;
-	Dir *d;
-	char buf[Nbuf];
-	char *p, *ep;
+	NetConnInfo *nci;
+	Passive *p;
 
-	USED(arg2);
+	USED(arg);
 
-	p = strchr(arg, '\r');
-	if(p){
-		logit("cr in file name", arg);
-		*p = 0;
+	p = &ftpd->conn.pasv;
+	if(p->inuse) {
+		close(p->afd);
+		p->inuse = 0;
 	}
 
-	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);
-	}
+	if(mountnet(ftpd) < 0)
+		return 0;
 
-	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->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();
 
-	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;
-			}
+	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);
 
-	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;
-	}
+	freenetconninfo(nci);
+	p->inuse = 1;
 
-	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);
-
-	if(n < 0)
-		logit("get %s %r after %d", arg, bytes);
-
-	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 
+portcmd(Ftpd *ftpd, char *arg)
 {
-	int dfd, n, i;
-	char buf[Nbuf];
+	char *field[7];
+	char data[64];
 
-	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;
-	}
+	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));
 
-	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");
+	return reply(ftpd->out, "200 Data port is %s", data);
 }
+
 int
-storecmd(char *arg)
+pwdcmd(Ftpd *ftpd, char *arg)
 {
-	int fd, rv;
+	USED(arg);
+	return reply(ftpd->out, "257 \"%s\" is the current directory", ftpd->user.cwd);
+}
 
-	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);
-	}
+int 
+quitcmd(Ftpd *ftpd, char *arg)
+{
+	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
-appendcmd(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);
+int 
+resetcmd(Ftpd *ftpd, char *arg)
+{
+	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");
 	}
-	seek(fd, 0, 2);
 
-	rv = asproc(store, arg, fd);
-	close(fd);
-	return rv;
+	return reply(ftpd->out, "350 Restarting at %lld");
 }
-int
-storeucmd(char *arg)
+
+int 
+retreivecmd(Ftpd *ftpd, char *arg)
 {
-	int fd, rv;
-	char name[Maxpath];
+	Dir *d;
+	Biobuf *fd, *data;
+	char *line;
+	char buf[4096];
+	long rsz;
 
-	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);
+	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);
 
-	rv = asproc(store, name, fd);
-	close(fd);
-	return rv;
-}
+	fd = Bopen(arg, OREAD);
+	if(!fd)
+		return reply(ftpd->out, "550 Error opening %s: %r", arg);
 
-int
-mkdircmd(char *name)
-{
-	int fd;
+	if(ftpd->offset != 0)
+		Bseek(fd, ftpd->offset, 0);
 
-	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);
+	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");
+	Bterm(stored);
+	closedata(ftpd, data, 0);
 
-	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;
+	logit("store: user %s file %s", ftpd->user.name, arg);
 
-	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);
-
-	return r;
+	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;
+	if(ftpd->user.loggedin)
+		return reply(ftpd->out, "530 Already logged in as %s", ftpd->user.name);
 
-	/*
-	 * 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(arg == nil)
+		return reply(ftpd->out, "530 User command needs username");
 
-	f = open(file, OWRITE);
-	if(f < 0)
-		return -1;
+	if(anonall)
+		ftpd->user.isnone = 1;
 
-	r = strlen(note);
-	if(write(f, note, r) != r) {
-		close(f);
-		return -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);
+void 
+main(int argc, char **argv)
+{
+	Ftpd ftpd;
+	char *cmd, *arg;
+	Cmd *t;
 
-	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);
+	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
 
-	for(sp = special; *sp; sp++){
-		p = strchr(path, *sp);
-		if(p)
-			*p = 0;
-	}
+	tmfmtinstall();
 
-	cleanname(s_to_c(rpath));
-	rpath->ptr = rpath->base+strlen(rpath->base);
+	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");
 
-	if(!accessok(s_to_c(rpath)))
-		return nil;
+	ftpd.in = mallocz(sizeof(Biobuf), 1);
+	ftpd.out = mallocz(sizeof(Biobuf), 1);
+	Binit(ftpd.in, 0, OREAD);
+	Binit(ftpd.out, 1, OWRITE);
 
-	return s_to_c(rpath);
-}
+	/* open logfile */
+	syslog(0, "ftp", nil);
 
-typedef struct Path Path;
-struct Path {
-	Path	*next;
-	String	*path;
-	int	inuse;
-	int	ok;
-};
+	if(certpath) {
+		ftpd.conn.cert = readcert(certpath, &ftpd.conn.certlen);
+		ftpd.conn.tls = mallocz(sizeof(TLSconn), 1);
 
-enum
-{
-	Maxlevel = 16,
-	Maxperlevel= 8,
-};
+		/* 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 *pathlevel[Maxlevel];
-
-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;
+		if(implicittls) {
+			dprint("dbg: implicit tls mode");
+			starttls(&ftpd);
 		}
-		/* 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;
-		}
 	}
 
-	/* 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";
+		/* get the arguments */
+		arg = strchr(cmd, ' ');
+		if(arg) {
+			*arg++ = '\0';
+			while(*arg == ' ')
+				arg++;
+			/* some clients always send a space */
+			if(*arg == '\0')
+				arg = nil;
+		}
 
-	if(level < 0)
-		return 1;
-	lvl = level;
-	if(lvl >= Maxlevel)
-		lvl = Maxlevel - 1;
+		/* find the cmd and execute it */
+		if(*cmd == '\0')
+			continue;
 
-	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;
-	}
+		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;
+			}
 
-	/*
-	 * 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));
+		/* 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);
 
-	return p->ok;
-}
-
-/*
- * check for a subdirectory containing .httplogin
- * at each level of the path.
- */
-int
-accessok(char *path)
-{
-	int level, r;
-	char *p;
-	String *npath;
-
-	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);
 }