diff options
author | foura <james@biobuf.link> | 2021-05-02 15:29:43 +0100 |
---|---|---|
committer | foura <james@biobuf.link> | 2021-05-02 15:29:43 +0100 |
commit | e72da62915b09d5673b0c0179ba8dfe045aeb8c3 (patch) | |
tree | 0b4e83b3abbdfff1421d8e5973c982eb5bc95f31 | |
parent | 9633c9fc65a833747a24cbd51f922afcf2859efd (diff) | |
download | plan9front-e72da62915b09d5673b0c0179ba8dfe045aeb8c3.tar.xz |
ip/ftpd: Add explict and implicit FTPS support.
Removed:
- Challenge reponse auth.
- Noworld login.
- Anonymous users writing files to /incoming.
-rw-r--r-- | sys/man/8/ipserv | 43 | ||||
-rw-r--r-- | sys/src/cmd/ip/ftpd.c | 2478 |
2 files changed, 844 insertions, 1677 deletions
diff --git a/sys/man/8/ipserv b/sys/man/8/ipserv index 770d02b10..db934e481 100644 --- a/sys/man/8/ipserv +++ b/sys/man/8/ipserv @@ -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 diff --git a/sys/src/cmd/ip/ftpd.c b/sys/src/cmd/ip/ftpd.c index 6661189e9..7519bc6bc 100644 --- a/sys/src/cmd/ip/ftpd.c +++ b/sys/src/cmd/ip/ftpd.c @@ -1,893 +1,440 @@ #include <u.h> #include <libc.h> #include <bio.h> -#include <auth.h> #include <ip.h> #include <libsec.h> -#include <String.h> +#include <auth.h> +#include <String.h> #include "glob.h" -enum -{ - /* telnet control character */ - Iac= 255, - - /* 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 */ +enum { + Tascii, + Timage, - 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; } - return 0; } -/* - * run a command to filter a tail - */ -int -transfer(char *cmd, char *a1, char *a2, char *a3, int image) +int +mountnet(Ftpd *ftpd) { - int n, dfd, fd, bytes, eofs, pid; - int pfd[2]; - char buf[Nbuf], *p; - Waitmsg *w; - - reply("150 Opening data connection for %s (%s)", cmd, data); - dfd = dialdata(); - if(dfd < 0) - return reply("425 Error opening data connection: %r"); - - if(pipe(pfd) < 0) - return reply("520 Internal Error: %r"); - - bytes = 0; - switch(pid = rfork(RFFDG|RFPROC|RFNAMEG)){ - case -1: - return reply("450 Out of processes: %r"); - case 0: - logit("running %s %s %s %s pid %d", - cmd, a1?a1:"", a2?a2:"" , a3?a3:"",getpid()); - close(pfd[1]); - close(dfd); - dup(pfd[0], 1); - dup(pfd[0], 2); - if(isnone){ - fd = open("#s/boot", ORDWR); - if(fd < 0 - || bind("#/", "/", MAFTER) == -1 - || amount(fd, "/bin", MREPL, "") == -1 - || bind("#c", "/dev", MAFTER) == -1 - || bind(bindir, "/bin", MREPL) == -1) - exits("building name space"); - close(fd); - } - execl(cmd, cmd, a1, a2, a3, nil); - exits(cmd); - default: - close(pfd[0]); - eofs = 0; - while((n = read(pfd[1], buf, sizeof buf)) >= 0){ - if(n == 0){ - if(eofs++ > 5) - break; - else - continue; - } - eofs = 0; - p = buf; - if(offset > 0){ - if(n > offset){ - p = buf+offset; - n -= offset; - offset = 0; - } else { - offset -= n; - continue; - } - } - if(!image) - n = crlfwrite(dfd, p, n); - else - n = write(dfd, p, n); - if(n < 0){ - postnote(PNPROC, pid, "kill"); - bytes = -1; - break; - } - bytes += n; - } - close(pfd[1]); - close(dfd); - break; + if(bind("#/", "/", MAFTER) == -1) { + reply(ftpd->out, "500 can't bind #/ to /: %r"); + return -1; } - /* wait for this command to finish */ - for(;;){ - w = wait(); - if(w == nil || w->pid == pid) - break; - free(w); - } - if(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(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; } - free(w); - reply("226 Transfer complete"); - return bytes; -} - -int -optscmd(char *arg) -{ - char *p; - if(arg == 0 || *arg == 0){ - reply("501 Syntax error in parameters or arguments"); - return 0; - } - if(p = strchr(arg, ' ')) - *p = 0; - if(cistrcmp(arg, "UTF-8") == 0 || cistrcmp(arg, "UTF8") == 0){ - reply("200 Command okay"); - return 0; - } - reply("502 %s option not implemented", arg); return 0; } -/* - * just reply OK - */ -int -nopcmd(char *arg) +void +unmountnet(void) { - USED(arg); - reply("510 Plan 9 FTP daemon still alive"); - return 0; + unmount(nil, "/net"); + unmount("#/", "/"); } -/* - * login as user - */ -int -loginuser(char *user, char *nsfile, int gotoslash) +Biobuf * +dialdata(Ftpd *ftpd, int read) { - logit("login %s %s %s %s", user, mailaddr, nci->rsys, nsfile); - if(nsfile != nil && newns(user, nsfile) < 0){ - logit("namespace file %s does not exist", nsfile); - return reply("530 Not logged in: login out of service"); - } - 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"); -} + Biobuf *bio; + TLSconn *tls; + int fd, cfd; + char ldir[40]; -static void -slowdown(void) -{ - static ulong pause; - - if (pause) { - sleep(pause); /* deter guessers */ - if (pause < (1UL << 20)) - pause *= 2; - } else - pause = 1000; -} + if(mountnet(ftpd) < 0) + return nil; -/* - * 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"); + 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); + } } - 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(fd < 0) { + reply(ftpd->out, "425 Error opening data connection"); + unmountnet(); + return nil; } - 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"); + reply(ftpd->out, "150 Opened data connection"); + + 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; } - /* 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); + + dprint("dbg: tlsserver done"); } -} -/* - * 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); + unmountnet(); + if(read) + bio = Bfdopen(fd, OREAD); + else + bio = Bfdopen(fd, OWRITE); + bio->aux = tls; + + return bio; } -/* - * chdir - */ -int -cwdcmd(char *dir) +void +closedata(Ftpd *ftpd, Biobuf *bio, int fail) { - char *rp; - char buf[Maxpath]; + TLSconn *conn; - /* 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); + conn = bio->aux; + + Bflush(bio); + Bterm(bio); + if(!fail) + reply(ftpd->out, "226 Transfer complete"); + + if(conn) { + free(conn->cert); + free(conn); + } } -/* - * chdir .. - */ -int -cdupcmd(char *dp) +int +starttls(Ftpd *ftpd) { - USED(dp); - return cwdcmd(".."); + 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; } int -quitcmd(char *arg) +abortcmd(Ftpd *ftpd, char *arg) { USED(arg); - reply("200 Bye"); - if(pid) - postnote(PNPROC, pid, "kill"); - return -1; + + if(ftpd->cmdpid){ + if(postnote(PNPROC, ftpd->cmdpid, "kill") == 0) + reply(ftpd->out, "426 Command aborted"); + else + logit("postnote pid %d %r", ftpd->cmdpid); + } + return reply(ftpd->out, "226 Abort processed"); } -int -typecmd(char *arg) +int +authcmd(Ftpd *ftpd, char *arg) { - int c; - char *x; + if((cistrcmp(arg, "TLS") == 0) || (cistrcmp(arg, "TLS-C") == 0) || (cistrcmp(arg, "SSL") == 0)) { - x = arg; - if(arg == 0) - return reply("501 Type command needs arguments"); + if(!ftpd->conn.tls) + return reply(ftpd->out, "431 tls not enabled"); - while(c = *arg++){ - switch(tolower(c)){ - case 'a': - type = Tascii; - break; - case 'i': - case 'l': - type = Timage; - break; - case '8': - case ' ': - case 'n': - case 't': - case 'c': - break; - default: - return reply("501 Unimplemented type %s", x); - } + reply(ftpd->out, "234 starting tls"); + if(starttls(ftpd) < 0) + return reply(ftpd->out, "431 tls failed"); + } else { + return reply(ftpd->out, "502 security method %s not understood", arg); } - return reply("200 Type %s", type==Tascii ? "Ascii" : "Image"); + + return 0; } -int -modecmd(char *arg) +int +cwdcmd(Ftpd *ftpd, char *arg) { - if(arg == 0) - return reply("501 Mode command needs arguments"); - while(*arg){ - switch(tolower(*arg)){ - case 's': - mode = Mstream; - break; - default: - return reply("501 Unimplemented mode %c", *arg); - } - arg++; + char buf[Maxpath]; + + if(!arg || *arg == '\0') { + if(ftpd->user.isnone) + snprint(buf, Maxpath, "/"); + else + snprint(buf, Maxpath, "/usr/%s", ftpd->user.name); + } else { + strncpy(buf, arg, Maxpath); + cleanname(buf); } - return reply("200 Stream mode"); + + if(chdir(buf) < 0) + return reply(ftpd->out, "550 CWD failed: %r"); + + getwd(ftpd->user.cwd, Maxpath); + return reply(ftpd->out, "200 Directory changed to %s", ftpd->user.cwd); } -int -structcmd(char *arg) +int +deletecmd(Ftpd *ftpd, char *arg) { - if(arg == 0) - return reply("501 Struct command needs arguments"); - for(; *arg; arg++){ - switch(tolower(*arg)){ - case 'f': - structure = Sfile; - break; - default: - return reply("501 Unimplemented structure %c", *arg); - } - } - return reply("200 File structure"); + if(!arg) + return reply(ftpd->out, "501 Rmdir/Delete command needs an argument"); + if(ftpd->user.isnone) + return reply(ftpd->out, "550 Permission denied"); + if(remove(cleanname(arg)) < 0) + return reply(ftpd->out, "550 Can't remove %s: %r", arg); + else + return reply(ftpd->out, "226 \"%s\" removed", arg); } -int -portcmd(char *arg) +int +featcmd(Ftpd *ftpd, char *arg) { - char *field[7]; - int n; - - if(arg == 0) - return reply("501 Port command needs arguments"); - n = getfields(arg, field, 7, 0, ", "); - if(n != 6) - return reply("501 Incorrect port specification"); - snprint(data, sizeof data, "tcp!%.3s.%.3s.%.3s.%.3s!%d", field[0], field[1], field[2], - field[3], atoi(field[4])*256 + atoi(field[5])); - return reply("200 Data port is %s", data); + USED(arg); + reply(ftpd->out, "211-Features supported"); + reply(ftpd->out, " UTF8"); + reply(ftpd->out, " PBSZ"); + reply(ftpd->out, " PROT"); + reply(ftpd->out, " AUTH TLS"); + reply(ftpd->out, " MLST Type*;Size*;Modify*;Unix.groupname*;UNIX.ownername*;"); + return reply(ftpd->out, "211 End"); } -int -mountnet(void) +int +dircmp(void *va, void *vb) { - int rv; - - rv = 0; - - if(bind("#/", "/", MAFTER) == -1){ - logit("can't bind #/ to /: %r"); - return reply("500 can't bind #/ to /: %r"); - } + Dir *a, *b; - 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("#/", "/"); - } + a = va; + b = vb; - return rv; + return strcmp(a->name, b->name); } void -unmountnet(void) +listdir(Ftpd *ftpd, Biobuf *data, char *path, void (*fn)(Biobuf *, Dir *d, char *dirname)) { - unmount(0, "/net"); - unmount("#/", "/"); + Dir *dirbuf; + int fd; + long ndirs; + long i; + + fd = open(path, OREAD); + if(!fd) + return; + + ndirs = dirreadall(fd, &dirbuf); + if(ndirs < 1) + return; + close(fd); + + qsort(dirbuf, ndirs, sizeof(Dir), dircmp); + for(i=0;i<ndirs;i++) + (*fn)(data, &dirbuf[i], (strcmp(path, ftpd->user.cwd) == 0 ? nil : path)); + + free(dirbuf); } int -pasvcmd(char *arg) +list(Ftpd *ftpd, char *arg, void (*fn)(Biobuf *, Dir *d, char *dirname)) { - NetConnInfo *nnci; - Passive *p; - - USED(arg); - p = &passive; + Biobuf *data; + int argc, i; + char *argv[32]; + Globlist *gl; + char *path; + Dir *d; - if(p->inuse){ - close(p->afd); - p->inuse = 0; + if(arg) { + argc = getfields(arg, argv, sizeof(argv)-1, 1, " \t"); + } else { + argc = 1; + argv[0] = ftpd->user.cwd; } - if(mountnet() < 0) - return 0; + data = dialdata(ftpd, 0); + if(!data) + return reply(ftpd->out, "500 List failed: couldn't dial data"); - p->afd = announce("tcp!*!0", passive.adir); - if(p->afd < 0){ - unmountnet(); - return reply("500 No free ports"); - } - nnci = getnetconninfo(p->adir, -1); - unmountnet(); + for(i=0;i<argc;i++) { + gl = glob(argv[i]); + if(!gl) + continue; - /* parse the local address */ - if(debug) - logit("local sys is %s", nci->lsys); - parseip(p->ipaddr, nci->lsys); - if(ipcmp(p->ipaddr, v4prefix) == 0 || ipcmp(p->ipaddr, IPnoaddr) == 0) - parseip(p->ipaddr, nci->lsys); - p->port = atoi(nnci->lserv); + while(path = globiter(gl)) { + cleanname(path); - freenetconninfo(nnci); - p->inuse = 1; + logit("list: path %s user %s", path, ftpd->user.name); - return reply("227 Entering Passive Mode (%d,%d,%d,%d,%d,%d)", - p->ipaddr[IPv4off+0], p->ipaddr[IPv4off+1], p->ipaddr[IPv4off+2], p->ipaddr[IPv4off+3], - p->port>>8, p->port&0xff); -} + d = dirstat(path); + if(d->mode & DMDIR) + listdir(ftpd, data, path, fn); + else + (*fn)(data, d, nil); -enum -{ - Narg=32, -}; -int Cflag, rflag, tflag, Rflag; -int maxnamelen; -int col; + free(d); + } + } + + closedata(ftpd, data, 0); + + return 0; +} -char* +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; - fd = open("#c/user", OREAD); + if(!arg) + reply(ftpd->out, "501 Mkdir command requires argument."); + if(ftpd->user.isnone) + reply(ftpd->out, "550 Permission denied"); + + cleanname(arg); + fd = create(arg, OREAD, DMDIR|0755); if(fd < 0) - return 1; - n = read(fd, buf, sizeof buf - 1); - if(n > 0){ - buf[n] = 0; - if(strcmp(buf, "none") == 0) - n = -1; - } + return reply(ftpd->out, "550 Can't create %s: %r", arg); close(fd); - return n > 0; + + return reply(ftpd->out, "226 %s created", arg); } -int -sitecmd(char *arg) +void +mlsdprint(Biobuf *data, Dir *d, char*) { - char *f[4]; - int nf, r; - Dir *d; + Tm mtime; - if(arg == 0) - return reply("501 bad site command"); - nf = tokenize(arg, f, nelem(f)); - if(nf != 3 || cistrcmp(f[0], "chmod") != 0) - return reply("501 bad site command"); - if(!oksiteuser()) - return reply("550 Permission denied"); - d = dirstat(f[2]); - if(d == nil) - return reply("501 site chmod: file does not exist"); - d->mode &= ~0777; - d->mode |= strtoul(f[1], 0, 8) & 0777; - r = dirwstat(f[2], d); - free(d); - if(r < 0) - return reply("550 Permission denied %r"); - return reply("200 very well, then"); - } - -/* - * 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); + } + + to = dirstat(arg); + if(to) { + free(from); free(to); + return reply(ftpd->out, "550 Can't rename: target %s exists", arg); } - return reply("226 Abort processed"); + + 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); + + logit("store: user %s file %s", ftpd->user.name, arg); - return r; + return 0; } -/* - * to dial out we need the network file system in our - * name space. - */ int -dialdata(void) +typecmd(Ftpd *ftpd, char *arg) { - int fd, cfd; - char ldir[40]; - char err[ERRMAX]; + int c; + char *x; - if(mountnet() < 0) - return -1; + if(!arg) + return reply(ftpd->out, "501 Type command needs an argument"); - if(!passive.inuse) - fd = dial(data, "20", 0, 0); - else { - fd = -1; - alarm(5*60*1000); - cfd = listen(passive.adir, ldir); - alarm(0); - if(cfd >= 0){ - fd = accept(cfd, ldir); - close(cfd); + x = arg; + while(c = *x++) { + switch(tolower(c)) { + case 'a': + ftpd->type = Tascii; + break; + case 'i': + case 'l': + ftpd->type = Timage; + break; + case '8': + case ' ': + case 'n': + case 't': + case 'c': + break; + default: + return reply(ftpd->out, "501 Unimplemented type %s", arg); } } - err[0] = 0; - errstr(err, sizeof err); - if(fd < 0) - logit("can't dial %s: %s", data, err); - unmountnet(); - errstr(err, sizeof err); - return fd; + + return reply(ftpd->out, "200 Type %s", (ftpd->type == Tascii ? "Ascii" : "Image")); } int -postnote(int group, int pid, char *note) +usercmd(Ftpd *ftpd, char *arg) { - char file[128]; - int f, r; - - /* - * 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); } |