shithub: riscv

ref: b7105fe0662b57689f629ac768dd084f8a1589a4
dir: /sys/src/cmd/ip/ftpfs/proto.c/

View raw version
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <ip.h>
#include <mp.h>
#include <libsec.h>
#include <auth.h>
#include <fcall.h>
#include <ctype.h>
#include <String.h>
#include "ftpfs.h"

enum
{
	/* return codes */
	Extra=		1,
	Success=	2,
	Incomplete=	3,
	TempFail=	4,
	PermFail=	5,
	Impossible=	6,
};

Node	*remdir;		/* current directory on remote machine */
Node	*remroot;		/* root directory on remote machine */

int	ctlfd;			/* fd for control connection */
Biobuf	ctlin;			/* input buffer for control connection */
Biobuf	stdin;			/* input buffer for standard input */
Biobuf	dbuf;			/* buffer for data connection */
char	msg[512];		/* buffer for replies */
char	net[Maxpath];		/* network for connections */
int	listenfd;		/* fd to listen on for connections */
char	netdir[Maxpath];
int	os, defos;
char	topsdir[64];		/* name of listed directory for TOPS */
String	*remrootpath;	/* path on remote side to remote root */
char	*user;
int	nopassive;
long	lastsend;
extern int usetls;

static void	sendrequest(char*, char*);
static int	getreply(Biobuf*, char*, int, int);
static int	active(int, Biobuf**, char*, char*);
static int	passive(int, Biobuf**, char*, char*);
static int	data(int, Biobuf**, char*, char*);
static int	port(void);
static void	ascii(void);
static void	image(void);
static void	unixpath(Node*, String*);
static void	vmspath(Node*, String*);
static void	mvspath(Node*, String*);
static Node*	vmsdir(char*);
static int	getpassword(char*, char*);
static int	nw_mode(char dirlet, char *s);

static void
starttls(int *fd)
{
	TLSconn conn;
	
	memset(&conn, 0, sizeof(conn));
	if((*fd = tlsClient(*fd, &conn)) < 0)
		fatal("starting tls: %r");
	free(conn.cert);
	free(conn.sessionID);
}

/*
 *  connect to remote server, default network is "tcp/ip"
 */
void
hello(char *dest)
{
	char *p;
	char dir[Maxpath];

	Binit(&stdin, 0, OREAD);	/* init for later use */

	ctlfd = dial(netmkaddr(dest, "tcp", "ftp"), 0, dir, 0);
	if(ctlfd < 0){
		fprint(2, "can't dial %s: %r\n", dest);
		exits("dialing");
	}
		
	Binit(&ctlin, ctlfd, OREAD);

	/* remember network for the data connections */
	p = strrchr(dir+1, '/');
	if(p == 0)
		fatal("wrong dial(2) linked with ftp");
	*p = 0;
	safecpy(net, dir, sizeof(net));

	/* wait for hello from other side */
	if(getreply(&ctlin, msg, sizeof(msg), 1) != Success)
		fatal("bad hello");
	if(strstr(msg, "Plan 9"))
		os = Plan9;

	if(usetls){
		sendrequest("AUTH", "TLS");
		if(getreply(&ctlin, msg, sizeof(msg), 1) != Success)
			fatal("bad auth tls");

		starttls(&ctlfd);
	
		Binit(&ctlin, ctlfd, OREAD);

		sendrequest("PBSZ", "0");
		if(getreply(&ctlin, msg, sizeof(msg), 1) != Success)
			fatal("bad pbsz 0");
		sendrequest("PROT", "P");
		if(getreply(&ctlin, msg, sizeof(msg), 1) != Success)
			fatal("bad prot p");
	}
}

/*
 *  login to remote system
 */
void
rlogin(char *rsys, char *keyspec)
{
	char *line;
	char pass[128];
	UserPasswd *up;

	up = nil;
	for(;;){
		if(up == nil && os != Plan9)
			up = auth_getuserpasswd(auth_getkey, "proto=pass server=%s service=ftp %s", rsys, keyspec);
		if(up != nil){
			sendrequest("USER", up->user);
		} else {
			print("User[default = %s]: ", user);
			line = Brdline(&stdin, '\n');
			if(line == 0)
				exits(0);
			line[Blinelen(&stdin)-1] = 0;
			if(*line){
				free(user);
				user = strdup(line);
			}
			sendrequest("USER", user);
		}
		switch(getreply(&ctlin, msg, sizeof(msg), 1)){
		case Success:
			goto out;
		case Incomplete:
			break;
		case TempFail:
		case PermFail:
			continue;
		}

		if(up != nil){
			sendrequest("PASS", up->passwd);
		} else {
			if(getpassword(pass, pass+sizeof(pass)) < 0)
				exits(0);
			sendrequest("PASS", pass);
		}
		if(getreply(&ctlin, msg, sizeof(msg), 1) == Success){
			if(strstr(msg, "Sess#"))
				defos = MVS;
			break;
		}
	}
out:
	if(up != nil){
		memset(up, 0, sizeof(*up));
		free(up);
	}
}

/*
 *  login to remote system with given user name and password.
 */
void
clogin(char *cuser, char *cpassword)
{
	free(user);
	user = strdup(cuser);
	if (strcmp(user, "anonymous") != 0 &&
	    strcmp(user, "ftp") != 0)
		fatal("User must be 'anonymous' or 'ftp'");
	sendrequest("USER", user);
	switch(getreply(&ctlin, msg, sizeof(msg), 1)){
	case Success:
		return;
	case Incomplete:
		break;
	case TempFail:
	case PermFail:
		fatal("login failed");
	}
	if (cpassword == 0)
		fatal("password needed");
	sendrequest("PASS", cpassword);
	if(getreply(&ctlin, msg, sizeof(msg), 1) != Success)
		fatal("password failed");
	if(strstr(msg, "Sess#"))
		defos = MVS;
	return;
}

/*
 *  find out about the other side.  go to it's root if requested.  set
 *  image mode if a Plan9 system.
 */
void
preamble(char *mountroot)
{
	char *p, *ep;
	int rv;
	OS *o;

	/*
	 *  create a root directory mirror
	 */
	remroot = newnode(0, s_copy("/"));
	remroot->d->qid.type = QTDIR;
	remroot->d->mode = DMDIR|0777;
	remdir = remroot;

	/*
	 *  get system type
	 */
	sendrequest("SYST", nil);
	switch(getreply(&ctlin, msg, sizeof(msg), 1)){
	case Success:
		for(o = oslist; o->os != Unknown; o++)
			if(strncmp(msg+4, o->name, strlen(o->name)) == 0)
				break;
		os = o->os;
		if(os == NT)
			os = Unix;
		break;
	default:
		os = defos;
		break;
	}
	if(os == Unknown)
		os = defos;

	remrootpath = s_reset(remrootpath);
	switch(os){
	case NetWare:
              /*
               * Request long, rather than 8.3 filenames,
               * where the Servers & Volume support them.
               */
              sendrequest("SITE LONG", nil);
              getreply(&ctlin, msg, sizeof(msg), 0);
              /* FALL THRU */
	case Unix:
	case Plan9:
		/*
		 *  go to the remote root, if asked
		 */
		if(mountroot){
			sendrequest("CWD", mountroot);
			getreply(&ctlin, msg, sizeof(msg), 0);
		} else {
			s_append(remrootpath, "/usr/");
			s_append(remrootpath, user);
		}

		/*
		 *  get the root directory
		 */
		sendrequest("PWD", nil);
		rv = getreply(&ctlin, msg, sizeof(msg), 1);
		if(rv == PermFail){
			sendrequest("XPWD", nil);
			rv = getreply(&ctlin, msg, sizeof(msg), 1);
		}
		if(rv == Success){
			p = strchr(msg, '"');
			if(p){
				p++;
				ep = strchr(p, '"');
				if(ep){
					*ep = 0;
					s_append(s_reset(remrootpath), p);
				}
			}
		}

		break;
	case Tops:
	case VM:
		/*
		 *  top directory is a figment of our imagination.
		 *  make it permanently cached & valid.
		 */
		CACHED(remroot);
		VALID(remroot);
		remroot->d->atime = time(0) + 100000;

		/*
		 *  no initial directory.  We are in the
		 *  imaginary root.
		 */
		remdir = newtopsdir("???");
		topsdir[0] = 0;
		if(os == Tops && readdir(remdir) >= 0){
			CACHED(remdir);
			if(*topsdir)
				remdir->remname = s_copy(topsdir);
			VALID(remdir);
		}
		break;
	case VMS:
		/*
		 *  top directory is a figment of our imagination.
		 *  make it permanently cached & valid.
		 */
		CACHED(remroot);
		VALID(remroot);
		remroot->d->atime = time(0) + 100000;

		/*
		 *  get current directory
		 */
		sendrequest("PWD", nil);
		rv = getreply(&ctlin, msg, sizeof(msg), 1);
		if(rv == PermFail){
			sendrequest("XPWD", nil);
			rv = getreply(&ctlin, msg, sizeof(msg), 1);
		}
		if(rv == Success){
			p = strchr(msg, '"');
			if(p){
				p++;
				ep = strchr(p, '"');
				if(ep){
					*ep = 0;
					remroot = remdir = vmsdir(p);
				}
			}
		}
		break;
	case MVS:
		usenlst = 1;
		break;
	}

	if(os == Plan9)
		image();
}

static void
ascii(void)
{
	sendrequest("TYPE A", nil);
	switch(getreply(&ctlin, msg, sizeof(msg), 0)){
	case Success:
		break;
	default:
		fatal("can't set type to ascii");
	}
}

static void
image(void)
{
	sendrequest("TYPE I", nil);
	switch(getreply(&ctlin, msg, sizeof(msg), 0)){
	case Success:
		break;
	default:
		fatal("can't set type to image/binary");
	}
}

/*
 *  decode the time fields, return seconds since epoch began
 */
char *monthchars = "janfebmaraprmayjunjulaugsepoctnovdec";
static Tm now;

static ulong
cracktime(char *month, char *day, char *yr, char *hms)
{
	Tm tm;
	int i;
	char *p;


	/* default time */
	if(now.year == 0)
		now = *localtime(time(0));
	tm = now;
	tm.yday = 0;

	/* convert ascii month to a number twixt 1 and 12 */
	if(*month >= '0' && *month <= '9'){
		tm.mon = atoi(month) - 1;
		if(tm.mon < 0 || tm.mon > 11)
			tm.mon = 5;
	} else {
		for(p = month; *p; p++)
			*p = tolower(*p);
		for(i = 0; i < 12; i++)
			if(strncmp(&monthchars[i*3], month, 3) == 0){
				tm.mon = i;
				break;
			}
	}

	tm.mday = atoi(day);

	if(hms){
		tm.hour = strtol(hms, &p, 0);
		if(*p == ':'){
			tm.min = strtol(p+1, &p, 0);
			if(*p == ':')
				tm.sec = strtol(p+1, &p, 0);
		}
		if(tolower(*p) == 'p')
			tm.hour += 12;
	}

	if(yr){
		tm.year = atoi(yr);
		if(tm.year >= 1900)
			tm.year -= 1900;
	} else {
		if(tm.mon > now.mon || (tm.mon == now.mon && tm.mday > now.mday+1))
			tm.year--;
	}

	/* convert to epoch seconds */
	return tm2sec(&tm);
}

/*
 *  decode a Unix or Plan 9 file mode
 */
static ulong
crackmode(char *p)
{
	ulong flags;
	ulong mode;
	int i;

	flags = 0;
	switch(strlen(p)){
	case 10:	/* unix and new style plan 9 */
		switch(*p){
		case 'l':
			return DMSYML|0777;
		case 'd':
			flags |= DMDIR;
		case 'a':
			flags |= DMAPPEND;
		}
		p++;
		if(p[2] == 'l')
			flags |= DMEXCL;
		break;
	case 11:	/* old style plan 9 */
		switch(*p++){
		case 'd':
			flags |= DMDIR;
			break;
		case 'a':
			flags |= DMAPPEND;
			break;
		}
		if(*p++ == 'l')
			flags |= DMEXCL;
		break;
	default:
		return DMDIR|0777;
	}
	mode = 0;
	for(i = 0; i < 3; i++){
		mode <<= 3;
		if(*p++ == 'r')
			mode |= DMREAD;
		if(*p++ == 'w')
			mode |= DMWRITE;
		if(*p == 'x' || *p == 's' || *p == 'S')
			mode |= DMEXEC;
		p++;
	}
	return mode | flags;
}

/*
 *  find first punctuation
 */
char*
strpunct(char *p)
{
	int c;

	for(;c = *p; p++){
		if(ispunct(c))
			return p;
	}
	return 0;
}

/*
 *  decode a Unix or Plan 9 directory listing
 */
static Dir*
crackdir(char *p, String **remname, int nlst)
{
	char *field[15];
	char *dfield[4];
	char *cp;
	String *s;
	int dn, n;
	Dir d, *dp;

	memset(&d, 0, sizeof(d));

	if(nlst != 0){
		field[0] = p;
		n = 1;
	} else {
		n = getfields(p, field, 15, 1, " \t");
		if(n > 2 && strcmp(field[n-2], "->") == 0)
			n -= 2;
	}

	switch(os){
	case TSO:
		cp = strchr(field[0], '.');
		if(cp){
			*cp++ = 0;
			s = s_copy(cp);
			d.uid = field[0];
		} else {
			s = s_copy(field[0]);
			d.uid = "TSO";
		}
		d.gid = "TSO";
		d.mode = 0666;
		d.length = 0;
		d.atime = 0;
		break;
	case OS½:
		s = s_copy(field[n-1]);
		d.uid = "OS½";
		d.gid = d.uid;
		d.mode = 0666;
		switch(n){
		case 5:
			if(strcmp(field[1], "DIR") == 0)
				d.mode |= DMDIR;
			d.length = atoll(field[0]);
			dn = getfields(field[2], dfield, 4, 1, "-");
			if(dn == 3)
				d.atime = cracktime(dfield[0], dfield[1], dfield[2], field[3]);
			break;
		}
		break;
	case Tops:
		if(n != 4){ /* tops directory name */
			safecpy(topsdir, field[0], sizeof(topsdir));
			return 0;
		}
		s = s_copy(field[3]);
		d.length = atoll(field[0]);
		d.mode = 0666;
		d.uid = "Tops";
		d.gid = d.uid;
		dn = getfields(field[1], dfield, 4, 1, "-");
		if(dn == 3)
			d.atime = cracktime(dfield[1], dfield[0], dfield[2], field[2]);
		else
			d.atime = time(0);
		break;
	case VM:
		switch(n){
		case 9:
			s = s_copy(field[0]);
			s_append(s, ".");
			s_append(s, field[1]);
			d.length = atoll(field[3]) * atoll(field[4]);
			if(*field[2] == 'F')
				d.mode = 0666;
			else
				d.mode = 0777;
			d.uid = "VM";
			d.gid = d.uid;
			dn = getfields(field[6], dfield, 4, 1, "/-");
			if(dn == 3)
				d.atime = cracktime(dfield[0], dfield[1], dfield[2], field[7]);
			else
				d.atime = time(0);
			break;
		case 1:
			s = s_copy(field[0]);
			d.uid = "VM";
			d.gid = d.uid;
			d.mode = 0777;
			d.atime = 0;
			break;
		default:
			return nil;
		}
		break;
	case VMS:
		switch(n){
		case 6:
			for(cp = field[0]; *cp; cp++)
				*cp = tolower(*cp);
			cp = strchr(field[0], ';');
			if(cp)
				*cp = 0;
			d.mode = 0666;
			cp = field[0] + strlen(field[0]) - 4;
			if(strcmp(cp, ".dir") == 0){
				d.mode |= DMDIR;
				*cp = 0;
			}
			s = s_copy(field[0]);
			d.length = atoll(field[1]) * 512;
			field[4][strlen(field[4])-1] = 0;
			d.uid = field[4]+1;
			d.gid = d.uid;
			dn = getfields(field[2], dfield, 4, 1, "/-");
			if(dn == 3)
				d.atime = cracktime(dfield[1], dfield[0], dfield[2], field[3]);
			else
				d.atime = time(0);
			break;
		default:
			return nil;
		}
		break;
	case NetWare:
		switch(n){
		case 8:		/* New style */
			s = s_copy(field[7]);
			d.uid = field[2];
			d.gid = d.uid;
			d.mode = nw_mode(field[0][0], field[1]);
			d.length = atoll(field[3]);
			if(strchr(field[6], ':'))
				d.atime = cracktime(field[4], field[5], nil, field[6]);
			else
				d.atime = cracktime(field[4], field[5], field[6], nil);
			break;
		case 9:
			s = s_copy(field[8]);
			d.uid = field[2];
			d.gid = d.uid;
			d.mode = 0666;
			if(*field[0] == 'd')
				d.mode |= DMDIR;
			d.length = atoll(field[3]);
			d.atime = cracktime(field[4], field[5], field[6], field[7]);
			break;
		case 1:
			s = s_copy(field[0]);
			d.uid = "none";
			d.gid = d.uid;
			d.mode = 0777;
			d.atime = 0;
			break;
		default:
			return nil;
		}
		break;
	case Unix:
	case Plan9:
	default:
		switch(n){
		case 8:		/* ls -lg */
			s = s_copy(field[7]);
			d.uid = field[2];
			d.gid = d.uid;
			d.mode = crackmode(field[0]);
			d.length = atoll(field[3]);
			if(strchr(field[6], ':'))
				d.atime = cracktime(field[4], field[5], 0, field[6]);
			else
				d.atime = cracktime(field[4], field[5], field[6], 0);
			break;
		case 9:		/* ls -l */
			s = s_copy(field[8]);
			d.uid = field[2];
			d.gid = field[3];
			d.mode = crackmode(field[0]);
			d.length = atoll(field[4]);
			if(strchr(field[7], ':'))
				d.atime = cracktime(field[5], field[6], 0, field[7]);
			else
				d.atime = cracktime(field[5], field[6], field[7], 0);
			break;
		case 10:	/* plan 9 */
			s = s_copy(field[9]);
			d.uid = field[3];
			d.gid = field[4];
			d.mode = crackmode(field[0]);
			d.length = atoll(field[5]);
			if(strchr(field[8], ':'))
				d.atime = cracktime(field[6], field[7], 0, field[8]);
			else
				d.atime = cracktime(field[6], field[7], field[8], 0);
			break;
		case 4:		/* a Windows_NT version */
			s = s_copy(field[3]);
			d.uid = "NT";
			d.gid = d.uid;
			if(strcmp("<DIR>", field[2]) == 0){
				d.length = 0;
				d.mode = DMDIR|0777;
			} else {
				d.mode = 0666;
				d.length = atoll(field[2]);
			}
			dn = getfields(field[0], dfield, 4, 1, "/-");
			if(dn == 3)
				d.atime = cracktime(dfield[0], dfield[1], dfield[2], field[1]);
			break;
		case 1:
			s = s_copy(field[0]);
			d.uid = "none";
			d.gid = d.uid;
			d.mode = 0777;
			d.atime = 0;
			break;
		default:
			return nil;
		}
	}
	d.muid = d.uid;
	d.qid.type = (d.mode & DMDIR) ? QTDIR : QTFILE;
	d.mtime = d.atime;
	if(ext && (d.qid.type & QTDIR) == 0)
		s_append(s, ext);
	d.name = s_to_c(s);

	/* allocate a freeable dir structure */
	dp = reallocdir(&d, 0);
	*remname = s;

	return dp;
}

/*
 *  probe files in a directory to see if they are directories
 */
/*
 *  read a remote directory
 */
int
readdir(Node *node)
{
	Biobuf *bp;
	char *line;
	Node *np;
	Dir *d;
	long n;
	int tries, x, files;
	static int uselist;
	int usenlist;
	String *remname;

	if(changedir(node) < 0)
		return -1;

	usenlist = 0;
	for(tries = 0; tries < 3; tries++){
		if(usenlist || usenlst)
			x = data(OREAD, &bp, "NLST", nil);
		else if(os == Unix && !uselist)
			x = data(OREAD, &bp, "LIST -l", nil);
		else
			x = data(OREAD, &bp, "LIST", nil);
		switch(x){
		case Extra:
			break;
/*		case TempFail:
			continue;
*/
		default:
			if(os == Unix && uselist == 0){
				uselist = 1;
				continue;
			}
			return seterr(nosuchfile);
		}
		files = 0;
		while(line = Brdline(bp, '\n')){
			n = Blinelen(bp);
			if(debug)
				write(2, line, n);
			if(n > 1 && line[n-2] == '\r')
				n--;
			line[n - 1] = 0;

			d = crackdir(line, &remname, (usenlist || usenlst));
			if(d == nil)
				continue;
			files++;
			np = extendpath(node, remname);
			d->qid.path = np->d->qid.path;
			d->qid.vers = np->d->qid.vers;
			d->type = np->d->type;
			d->dev = 1;			/* mark node as valid */
			if(os == MVS && node == remroot){
				d->qid.type = QTDIR;
				d->mode |= DMDIR;
			}
			free(np->d);
			np->d = d;
		}
		close(Bfildes(bp));

		switch(getreply(&ctlin, msg, sizeof(msg), 0)){
		case Success:
			if(files == 0 && !usenlst && !usenlist){
				usenlist = 1;
				continue;
			}
			if(files && usenlist)
				usenlst = 1;
			if(usenlst)
				node->chdirunknown = 1;
			return 0;
		case TempFail:
			break;
		default:
			return seterr(nosuchfile);
		}
	}
	return seterr(nosuchfile);
}

/*
 *  create a remote directory
 */
int
createdir(Node *node)
{
	if(changedir(node->parent) < 0)
		return -1;
	
	sendrequest("MKD", node->d->name);
	if(getreply(&ctlin, msg, sizeof(msg), 0) != Success)
		return -1;
	return 0;
}

/*
 *  change to a remote directory.
 */
int
changedir(Node *node)
{
	Node *to;
	String *cdpath;

	to = node;
	if(to == remdir)
		return 0;

	/* build an absolute path */
	switch(os){
	case Tops:
	case VM:
		switch(node->depth){
		case 0:
			remdir = node;
			return 0;
		case 1:
			cdpath = s_clone(node->remname);
			break;
		default:
			return seterr(nosuchfile);
		}
		break;
	case VMS:
		switch(node->depth){
		case 0:
			remdir = node;
			return 0;
		default:
			cdpath = s_new();
			vmspath(node, cdpath);
		}
		break;
	case MVS:
		if(node->depth == 0)
			cdpath = s_clone(remrootpath);
		else{
			cdpath = s_new();
			mvspath(node, cdpath);
		}
		break;
	default:
		if(node->depth == 0)
			cdpath = s_clone(remrootpath);
		else{
			cdpath = s_new();
			unixpath(node, cdpath);
		}
		break;
	}

	uncachedir(remdir, 0);

	/*
	 *  connect, if we need a password (Incomplete)
	 *  act like it worked (best we can do).
	 */
	sendrequest("CWD", s_to_c(cdpath));
	s_free(cdpath);
	switch(getreply(&ctlin, msg, sizeof(msg), 0)){
	case Success:
	case Incomplete:
		remdir = node;
		return 0;
	default:
		return seterr(nosuchfile);
	}
}

/*
 *  read a remote file
 */
int
readfile1(Node *node)
{
	Biobuf *bp;
	char buf[4*1024];
	long off;
	int n;
	int tries;

	if(changedir(node->parent) < 0)
		return -1;

	for(tries = 0; tries < 4; tries++){
		switch(data(OREAD, &bp, "RETR", s_to_c(node->remname))){
		case Extra:
			break;
		case TempFail:
			continue;
		default:
			return seterr(nosuchfile);
		}
		off = 0;
		while((n = read(Bfildes(bp), buf, sizeof buf)) > 0){
			if(filewrite(node, buf, off, n) != n){
				off = -1;
				break;
			}
			off += n;
		}
		if(off < 0)
			return -1;

		/* make sure a file gets created even for a zero length file */
		if(off == 0)
			filewrite(node, buf, 0, 0);

		close(Bfildes(bp));
		switch(getreply(&ctlin, msg, sizeof(msg), 0)){
		case Success:
			return off;
		case TempFail:
			continue;
		default:
			return seterr(nosuchfile);
		}
	}
	return seterr(nosuchfile);
}

int
readfile(Node *node)
{
	int rv, inimage;

	switch(os){
	case MVS:
	case Plan9:
	case Tops:
	case TSO:
		inimage = 0;
		break;
	default:
		inimage = 1;
		image();
		break;
	}

	rv = readfile1(node);

	if(inimage)
		ascii();
	return rv;
}

/*
 *  write back a file
 */
int
createfile1(Node *node)
{
	Biobuf *bp;
	char buf[4*1024];
	long off;
	int n;

	if(changedir(node->parent) < 0)
		return -1;

	if(data(OWRITE, &bp, "STOR", s_to_c(node->remname)) != Extra)
		return -1;
	for(off = 0; ; off += n){
		n = fileread(node, buf, off, sizeof(buf));
		if(n <= 0)
			break;
		write(Bfildes(bp), buf, n);
	}
	close(Bfildes(bp));
	if(getreply(&ctlin, msg, sizeof(msg), 0) != Success)
		return -1;
	return off;
}

int
createfile(Node *node)
{
	int rv;

	switch(os){
	case Plan9:
	case Tops:
		break;
	default:
		image();
		break;
	}
	rv = createfile1(node);
	switch(os){
	case Plan9:
	case Tops:
		break;
	default:
		ascii();
		break;
	}
	return rv;
}

/*
 *  remove a remote file
 */
int
removefile(Node *node)
{
	if(changedir(node->parent) < 0)
		return -1;
	
	sendrequest("DELE", s_to_c(node->remname));
	if(getreply(&ctlin, msg, sizeof(msg), 0) != Success)
		return -1;
	return 0;
}

/*
 *  remove a remote directory
 */
int
removedir(Node *node)
{
	if(changedir(node->parent) < 0)
		return -1;
	
	sendrequest("RMD", s_to_c(node->remname));
	if(getreply(&ctlin, msg, sizeof(msg), 0) != Success)
		return -1;
	return 0;
}

/*
 *  tell remote that we're exiting and then do it
 */
void
quit(void)
{
	sendrequest("QUIT", nil);
	getreply(&ctlin, msg, sizeof(msg), 0);
	exits(0);
}

/*
 *  send a request
 */
static void
sendrequest(char *a, char *b)
{
	char buf[2*1024];
	int n;

	n = strlen(a)+2+1;
	if(b != nil)
		n += strlen(b)+1;
	if(n >= sizeof(buf))
		fatal("proto request too long");
	strcpy(buf, a);
	if(b != nil){
		strcat(buf, " ");
		strcat(buf, b);
	}
	strcat(buf, "\r\n");
	n = strlen(buf);
	if(write(ctlfd, buf, n) != n)
		fatal("remote side hung up");
	if(debug)
		write(2, buf, n);
	lastsend = time(0);
}

/*
 *  replies codes are in the range [100, 999] and may contain multiple lines of
 *  continuation.
 */
static int
getreply(Biobuf *bp, char *msg, int len, int printreply)
{
	char *line;
	char *p;
	int rv;
	int i, n;

	while(line = Brdline(bp, '\n')){
		/* add line to message buffer, strip off \r */
		n = Blinelen(bp);
		if(n > 1 && line[n-2] == '\r'){
			n--;
			line[n-1] = '\n';
		}
		if(printreply && !quiet)
			write(1, line, n);
		else if(debug)
			write(2, line, n);
		if(n > len - 1)
			i = len - 1;
		else
			i = n;
		if(i > 0){
			memmove(msg, line, i);
			msg += i;
			len -= i;
			*msg = 0;
		}

		/* stop if not a continuation */
		rv = strtol(line, &p, 10);
		if(rv >= 100 && rv < 600 && p==line+3 && *p != '-')
			return rv/100;

		/* tell user about continuations */
		if(!debug && !quiet && !printreply)
			write(2, line, n);
	}

	fatal("remote side closed connection");
	return 0;
}

/*
 *  Announce on a local port and tell its address to the the remote side
 */
static int
port(void)
{
	char buf[256];
	int n, fd;
	char *ptr;
	uchar ipaddr[IPaddrlen];
	int port;

	/* get a channel to listen on, let kernel pick the port number */
	sprint(buf, "%s!*!0", net);
	listenfd = announce(buf, netdir);
	if(listenfd < 0)
		return seterr("can't announce");

	/* get the local address and port number */
	sprint(buf, "%s/local", netdir);
	fd = open(buf, OREAD);
	if(fd < 0)
		return seterr("opening %s: %r", buf);
	n = read(fd, buf, sizeof(buf)-1);
	close(fd);
	if(n <= 0)
		return seterr("opening %s/local: %r", netdir);
	buf[n] = 0;
	ptr = strchr(buf, ' ');
	if(ptr)
		*ptr = 0;
	ptr = strchr(buf, '!')+1;
	port = atoi(ptr);

	memset(ipaddr, 0, IPaddrlen);
	if (*net){
		strcpy(buf, net);
		ptr = strchr(buf +1, '/');
		if (ptr)
			*ptr = 0;
		myipaddr(ipaddr, buf);
	}

	/* tell remote side */
	sprint(buf, "PORT %d,%d,%d,%d,%d,%d", ipaddr[IPv4off+0], ipaddr[IPv4off+1],
		ipaddr[IPv4off+2], ipaddr[IPv4off+3], port>>8, port&0xff);
	sendrequest(buf, nil);
	if(getreply(&ctlin, msg, sizeof(msg), 0) != Success)
		return seterr(msg);
	return 0;
}

/*
 *  have server call back for a data connection
 */
static int
active(int mode, Biobuf **bpp, char *cmda, char *cmdb)
{
	int cfd, dfd, rv;
	char newdir[Maxpath];
	char datafile[Maxpath + 6];

	if(port() < 0)
		return TempFail;

	sendrequest(cmda, cmdb);

	rv = getreply(&ctlin, msg, sizeof(msg), 0);
	if(rv != Extra){
		close(listenfd);
		return rv;
	}

	/* wait for a new call */
	cfd = listen(netdir, newdir);
	if(cfd < 0)
		fatal("waiting for data connection");
	close(listenfd);

	/* open it's data connection and close the control connection */
	sprint(datafile, "%s/data", newdir);
	dfd = open(datafile, ORDWR);
	close(cfd);
	if(dfd < 0)
		fatal("opening data connection");

	if(usetls)
		starttls(&dfd);

	Binit(&dbuf, dfd, mode);
	*bpp = &dbuf;
	return Extra;
}

/*
 *  call out for a data connection
 */
static int
passive(int mode, Biobuf **bpp, char *cmda, char *cmdb)
{
	char msg[1024];
	char ds[1024];
	char *f[6];
	char *p;
	int x, fd;

	if(nopassive)
		return Impossible;

	sendrequest("PASV", nil);
	if(getreply(&ctlin, msg, sizeof(msg), 0) != Success){
		nopassive = 1;
		return Impossible;
	}

	/* get address and port number from reply, this is AI */
	p = strchr(msg, '(');
	if(p == 0){
		for(p = msg+3; *p; p++)
			if(isdigit(*p))
				break;
	} else
		p++;
	if(getfields(p, f, 6, 0, ",") < 6){
		if(debug)
			fprint(2, "passive mode protocol botch: %s\n", msg);
		werrstr("ftp protocol botch");
		nopassive = 1;
		return Impossible;
	}
	snprint(ds, sizeof(ds), "%s!%s.%s.%s.%s!%d", net,
		f[0], f[1], f[2], f[3],
		((atoi(f[4])&0xff)<<8) + (atoi(f[5])&0xff));

	/* open data connection */
	fd = dial(ds, 0, 0, 0);
	if(fd < 0){
		if(debug)
			fprint(2, "passive mode connect to %s failed: %r\n", ds);
		nopassive = 1;
		return TempFail;
	}

	/* tell remote to send a file */
	sendrequest(cmda, cmdb);
	x = getreply(&ctlin, msg, sizeof(msg), 0);
	if(x != Extra){
		close(fd);
		if(debug)
			fprint(2, "passive mode retrieve failed: %s\n", msg);
		werrstr("%s", msg);
		return x;
	}

	if(usetls)
		starttls(&fd);

	Binit(&dbuf, fd, mode);

	*bpp = &dbuf;
	return Extra;
}

static int
data(int mode, Biobuf **bpp, char* cmda, char *cmdb)
{
	int x;

	x = passive(mode, bpp, cmda, cmdb);
	if(x != Impossible)
		return x;
	return active(mode, bpp, cmda, cmdb);
}

/*
 *  used for keep alives
 */
void
nop(void)
{
	if(lastsend - time(0) < 15)
		return;
	sendrequest("PWD", nil);
	getreply(&ctlin, msg, sizeof(msg), 0);
}

/*
 *  turn a vms spec into a path
 */
static Node*
vmsextendpath(Node *np, char *name)
{
	np = extendpath(np, s_copy(name));
	if(!ISVALID(np)){
		np->d->qid.type = QTDIR;
		np->d->atime = time(0);
		np->d->mtime = np->d->atime;
		strcpy(np->d->uid, "who");
		strcpy(np->d->gid, "cares");
		np->d->mode = DMDIR|0777;
		np->d->length = 0;
		if(changedir(np) >= 0)
			VALID(np);
	}
	return np;
}
static Node*
vmsdir(char *name)
{
	char *cp;
	Node *np;
	char *oname;

	np = remroot;
	cp = strchr(name, '[');
	if(cp)
		strcpy(cp, cp+1);
	cp = strchr(name, ']');
	if(cp)
		*cp = 0;
	oname = name = strdup(name);
	if(name == 0)
		return 0;

	while(cp = strchr(name, '.')){
		*cp = 0;
		np = vmsextendpath(np, name);
		name = cp+1;
	}
	np = vmsextendpath(np, name);

	/*
	 *  walk back to first accessible directory
	 */
	for(; np->parent != np; np = np->parent)
		if(ISVALID(np)){
			CACHED(np->parent);
			break;
		}

	free(oname);
	return np;
}

/*
 *  walk up the tree building a VMS style path
 */
static void
vmspath(Node *node, String *path)
{
	char *p;
	int n;

	if(node->depth == 1){
		p = strchr(s_to_c(node->remname), ':');
		if(p){
			n = p - s_to_c(node->remname) + 1;
			s_nappend(path, s_to_c(node->remname), n);
			s_append(path, "[");
			s_append(path, p+1);
		} else {
			s_append(path, "[");
			s_append(path, s_to_c(node->remname));
		}
		s_append(path, "]");
		return;
	}
	vmspath(node->parent, path);
	s_append(path, ".");
	s_append(path, s_to_c(node->remname));
}

/*
 *  walk up the tree building a Unix style path
 */
static void
unixpath(Node *node, String *path)
{
	if(node == node->parent){
		s_append(path, s_to_c(remrootpath));
		return;
	}
	unixpath(node->parent, path);
	if(s_len(path) > 0 && strcmp(s_to_c(path), "/") != 0)
		s_append(path, "/");
	s_append(path, s_to_c(node->remname));
}

/*
 *  walk up the tree building a MVS style path
 */
static void
mvspath(Node *node, String *path)
{
	if(node == node->parent){
		s_append(path, s_to_c(remrootpath));
		return;
	}
	mvspath(node->parent, path);
	if(s_len(path) > 0)
		s_append(path, ".");
	s_append(path, s_to_c(node->remname));
}

static int
getpassword(char *buf, char *e)
{
	char *p;
	int c;
	int consctl, rv = 0;

	consctl = open("/dev/consctl", OWRITE);
	if(consctl >= 0)
		write(consctl, "rawon", 5);
	print("Password: ");
	e--;
	for(p = buf; p <= e; p++){
		c = Bgetc(&stdin);
		if(c < 0){
			rv = -1;
			goto out;
		}
		if(c == '\n' || c == '\r')
			break;
		*p = c;
	}
	*p = 0;
	print("\n");

out:
	if(consctl >= 0)
		close(consctl);
	return rv;
}

/*
 *  convert from latin1 to utf
 */
static char*
fromlatin1(char *from)
{
	char *p, *to;
	Rune r;
	int n;

	if(os == Plan9)
		return nil;

	/* don't convert if we don't have to */
	for(p = from; *p; p += n){
		n = chartorune(&r, p);
		if(r == Runeerror)
			break;
	}
	if(*p == 0)
		return nil;

	to = malloc(UTFmax*strlen(from)+2);
	if(to == nil)
		return nil;
	for(p = to; *from; from++){
		r = (*from) & 0xff;
		p += runetochar(p, &r);
	}
	*p = 0;
	return to;
}

Dir*
reallocdir(Dir *d, int dofree)
{
	Dir *dp;
	char *p;
	int nn, ng, nu, nm;
	char *utf;

	if(d->name == nil)
		d->name = "?name?";
	if(d->uid == nil)
		d->uid = "?uid?";
	if(d->gid == nil)
		d->gid = d->uid;
	if(d->muid == nil)
		d->muid = d->uid;
	
	utf = fromlatin1(d->name);
	if(utf != nil)
		d->name = utf;

	nn = strlen(d->name)+1;
	nu = strlen(d->uid)+1;
	ng = strlen(d->gid)+1;
	nm = strlen(d->muid)+1;
	dp = malloc(sizeof(Dir)+nn+nu+ng+nm);
	if(dp == nil){
		if(dofree)
			free(d);
		if(utf != nil)
			free(utf);
		return nil;
	}
	*dp = *d;
	p = (char*)&dp[1];
	strcpy(p, d->name);
	dp->name = p;
	p += nn;
	strcpy(p, d->uid);
	dp->uid = p;
	p += nu;
	strcpy(p, d->gid);
	dp->gid = p;
	p += ng;
	strcpy(p, d->muid);
	dp->muid = p;
	if(dofree)
		free(d);
	if(utf != nil)
		free(utf);
	return dp;
}

Dir*
dir_change_name(Dir *d, char *name)
{
	if(d->name && strlen(d->name) >= strlen(name)){
		strcpy(d->name, name);
		return d;
	}
	d->name = name;
	return reallocdir(d, 1);
}

Dir*
dir_change_uid(Dir *d, char *name)
{
	if(d->uid && strlen(d->uid) >= strlen(name)){
		strcpy(d->name, name);
		return d;
	}
	d->uid = name;
	return reallocdir(d, 1);
}

Dir*
dir_change_gid(Dir *d, char *name)
{
	if(d->gid && strlen(d->gid) >= strlen(name)){
		strcpy(d->name, name);
		return d;
	}
	d->gid = name;
	return reallocdir(d, 1);
}

Dir*
dir_change_muid(Dir *d, char *name)
{
	if(d->muid && strlen(d->muid) >= strlen(name)){
		strcpy(d->name, name);
		return d;
	}
	d->muid = name;
	return reallocdir(d, 1);
}

static int
nw_mode(char dirlet, char *s)		/* NetWare file mode mapping */
{
	int mode = 0777;

	if(dirlet == 'd')
		mode |= DMDIR;

	if (strlen(s) >= 10 && s[0] != '[' || s[9] != ']')
		return(mode);

	if (s[1] == '-')					/* can't read file */
		mode &= ~0444;
	if (dirlet == 'd' && s[6] == '-')			/* cannot scan dir */
		mode &= ~0444;
	if (s[2] == '-')					/* can't write file */
		mode &= ~0222;
	if (dirlet == 'd' && s[7] == '-' && s[3] == '-')	/* cannot create in, or modify dir */
		mode &= ~0222;

	return(mode);
}