shithub: riscv

ref: 403fef45c1a65aa74c449764e2857597fc03290a
dir: /sys/src/cmd/ip/ftpd.c/

View raw version
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <auth.h>
#include <ip.h>
#include <libsec.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,

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

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;
};

Cmd cmdtab[] =
{
	{ "abor",	abortcmd,	0, },
	{ "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 },
};

#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;

#define FTPLOG "ftp"

void
logit(char *fmt, ...)
{
	char buf[8192];
	va_list arg;

	va_start(arg, fmt);
	vseprint(buf, buf+sizeof(buf), 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");
}

/*
 *  read commands from the control stream and dispatch
 */
void
main(int argc, char **argv)
{
	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;

	va_start(arg, fmt);
	s = vseprint(buf, buf+sizeof(buf)-3, fmt, arg);
	va_end(arg);
	if(debug){
		*s = 0;
		logit("%s", buf);
	}
	*s++ = '\r';
	*s++ = '\n';
	write(1, buf, s - buf);
	return 0;
}

int
sodoff(void)
{
	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(;;){
			i = waitpid();
			if(i == pid || i < 0)
				break;
		}
	}

	switch(pid = rfork(RFFDG|RFPROC|RFNOTEG)){
	case -1:
		return reply("450 Out of processes: %r");
	case 0:
		(*f)(arg, arg2);
		exits(0);
	default:
		break;
	}
	return 0;
}

/*
 * run a command to filter a tail
 */
int
transfer(char *cmd, char *a1, char *a2, char *a3, int image)
{
	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;
	}

	/* 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);
	}
	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)
{
	USED(arg);
	reply("510 Plan 9 FTP daemon still alive");
	return 0;
}

/*
 *  login as user
 */
int
loginuser(char *user, char *nsfile, int gotoslash)
{
	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, "/");
	}
	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;

	if (pause) {
		sleep(pause);			/* deter guessers */
		if (pause < (1UL << 20))
			pause *= 2;
	} else
		pause = 1000;
}

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

	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");
	}
	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);
}

/*
 *  get a password, set up user if it works.
 */
int
passcmd(char *response)
{
	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(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);
	}
}

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

/*
 *  chdir
 */
int
cwdcmd(char *dir)
{
	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);
}

/*
 *  chdir ..
 */
int
cdupcmd(char *dp)
{
	USED(dp);
	return cwdcmd("..");
}

int
quitcmd(char *arg)
{
	USED(arg);
	reply("200 Bye");
	if(pid)
		postnote(PNPROC, pid, "kill");
	return -1;
}

int
typecmd(char *arg)
{
	int c;
	char *x;

	x = arg;
	if(arg == 0)
		return reply("501 Type command needs arguments");

	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");
}

int
modecmd(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++;
	}
	return reply("200 Stream mode");
}

int
structcmd(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");
}

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

int
mountnet(void)
{
	int rv;

	rv = 0;

	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;
}

void
unmountnet(void)
{
	unmount(0, "/net");
	unmount("#/", "/");
}

int
pasvcmd(char *arg)
{
	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 *p;

	strcpy(asc, "----------");
	if(DMDIR & m)
		asc[0] = 'd';
	if(DMAPPEND & m)
		asc[0] = 'a';
	else if(DMEXCL & m)
		asc[3] = 'l';

	for(p = asc+1; p < asc + 10; p += 3, m<<=3){
		if(m & 0400)
			p[0] = 'r';
		if(m & 0200)
			p[1] = 'w';
		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)
{
	int rv;
	Dir *a, *b;

	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;

	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);
}
void
list(char *arg, int lflag)
{
	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;

	dfd = dialdata();
	if(dfd < 0){
		reply("425 Error opening data connection: %r");
		return;
	}
	reply("150 Opened data connection (%s)", data);

	Binits(&bh, dfd, OWRITE, buf, sizeof(buf));
	if(argc == 0){
		argc = 1;
		argv = alist;
		argv[0] = ".";
	}

	for(i = 0; i < argc; i++){
		chdir(curdir);
		gl = glob(argv[i]);
		if(gl == nil)
			continue;

		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);

	reply("226 Transfer complete (list %s)", arg);
}
int
namelistcmd(char *arg)
{
	return asproc(list, arg, 0);
}
int
listcmd(char *arg)
{
	return asproc(list, arg, 1);
}

/*
 * fuse compatability
 */
int
oksiteuser(void)
{
	char buf[64];
	int fd, n;

	fd = open("#c/user", OREAD);
	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;
	}
	close(fd);
	return n > 0;
}

int
sitecmd(char *arg)
{
	char *f[4];
	int nf, r;
	Dir *d;

	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;

	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 the modify time of the file
 */
int
mdtmcmd(char *arg)
{
	Dir *d;
	Tm *t;
	int rv;

	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;
}

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

	return reply("350 Restarting at %lld. Send STORE or RETRIEVE", offset);
}

/*
 *  send a file to the user
 */
int
crlfwrite(int fd, char *p, int n)
{
	char *ep, *np;
	char buf[2*Nbuf];

	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(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);
	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;

	USED(arg2);

	p = strchr(arg, '\r');
	if(p){
		logit("cr in file name", arg);
		*p = 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);
	}

	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;
	}

	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;
			}

	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;
	}

	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);
}
int
retrievecmd(char *arg)
{
	if(arg == 0)
		return reply("501 Retrieve command requires an argument");
	arg = abspath(arg);
	if(arg == 0)
		return reply("550 Permission denied");

	return asproc(retrieve, arg, 0);
}

/*
 *  get a file from the user
 */
int
lfwrite(int fd, char *p, int n)
{
	char *ep, *np;
	char buf[Nbuf];

	for(np = buf, ep = p + n; p < ep; p++){
		if(*p != '\r')
			*np++ = *p;
	}
	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
storecmd(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;
}
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);
	}
	seek(fd, 0, 2);

	rv = asproc(store, arg, fd);
	close(fd);
	return rv;
}
int
storeucmd(char *arg)
{
	int fd, rv;
	char name[Maxpath];

	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;
}

int
mkdircmd(char *name)
{
	int fd;

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

int
delcmd(char *name)
{
	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);
}

/*
 *  kill off the last transfer (if the process still exists)
 */
int
abortcmd(char *arg)
{
	USED(arg);

	logit("abort pid %d", pid);
	if(pid){
		if(postnote(PNPROC, pid, "kill") == 0)
			reply("426 Command aborted");
		else
			logit("postnote pid %d %r", pid);
	}
	return reply("226 Abort processed");
}

int
systemcmd(char *arg)
{
	USED(arg);
	return reply("215 UNIX Type: L8 Version: Plan 9");
}

int
helpcmd(char *arg)
{
	int i;
	char buf[80];
	char *p, *e;

	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(p != buf)
		reply("214-%s", buf);
	reply("214 ");
	return 0;
}

/*
 *  renaming a file takes two commands
 */
static String *filepath;

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

	return r;
}

/*
 *  to dial out we need the network file system in our
 *  name space.
 */
int
dialdata(void)
{
	int fd, cfd;
	char ldir[40];
	char err[ERRMAX];

	if(mountnet() < 0)
		return -1;

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

int
postnote(int group, int pid, char *note)
{
	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;
	}

	f = open(file, OWRITE);
	if(f < 0)
		return -1;

	r = strlen(note);
	if(write(f, note, r) != r) {
		close(f);
		return -1;
	}
	close(f);
	return 0;
}

/*
 *  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 = "`;| ";

char*
abspath(char *origpath)
{
	char *p, *sp, *path;
	static String *rpath;

	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);

	for(sp = special; *sp; sp++){
		p = strchr(path, *sp);
		if(p)
			*p = 0;
	}

	cleanname(s_to_c(rpath));
	rpath->ptr = rpath->base+strlen(rpath->base);

	if(!accessok(s_to_c(rpath)))
		return nil;

	return s_to_c(rpath);
}

typedef struct Path Path;
struct Path {
	Path	*next;
	String	*path;
	int	inuse;
	int	ok;
};

enum
{
	Maxlevel = 16,
	Maxperlevel= 8,
};

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

void
linkpath(Path *p, int level)
{
	p->next = pathlevel[level];
	pathlevel[level] = p;
	p->inuse = 1;
}

void
addpath(Path *p, int level, int ok)
{
	p->ok = ok;
	p->next = pathlevel[level];
	pathlevel[level] = p;
}

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;
	}

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

	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++;
	}

	r = _accessok(npath, level-1);
	s_free(npath);

	return r;
}