~mcf/plan9front

0bb792012609243845ed7bcf281679a91de50c10 — foura 5 months ago df21fb7
ip/ftpd: Add explict and implicit FTPS support.

Removed:
- Challenge reponse auth.
- Noworld login.
- Anonymous users writing files to /incoming.
2 files changed, 849 insertions(+), 1682 deletions(-)

M sys/man/8/ipserv
M sys/src/cmd/ip/ftpd.c
M sys/man/8/ipserv => sys/man/8/ipserv +13 -30
@@ 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

M sys/src/cmd/ip/ftpd.c => sys/src/cmd/ip/ftpd.c +836 -1652
@@ 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,

	/* 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);
}