Goto sanos source index

//
// ftpd.c
//
// FTP daemon
//
// Copyright (C) 2002 Michael Ringgaard. All rights reserved.
// Portions Copyright (C) 1995-2000 Trolltech AS. 
// Portions Copyright (C) 2001 Arnt Gulbrandsen.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 
// 1. Redistributions of source code must retain the above copyright 
//    notice, this list of conditions and the following disclaimer.  
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.  
// 3. Neither the name of the project nor the names of its contributors
//    may be used to endorse or promote products derived from this software
//    without specific prior written permission. 
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
// SUCH DAMAGE.
// 

#include <os.h>
#include <inifile.h>

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <dirent.h>
#include <time.h>
#include <pwd.h>

#define STOPPED 0
#define RUNNING 1

int port;

int port;
int state = STOPPED;
int sock = NOHANDLE;
int logging = 2;
int timeout = 900 * 1000;

struct reply {
  struct reply *next;
  char line[1];
};

struct ftpstate {
  int ctrlsock;
  int datasock;
  int replycode;
  struct reply *firstreply;
  struct reply *lastreply;
  FILE *in;
  FILE *out;
  struct sockaddr_in peer;
  char cmd[MAXPATH + 32];
  char wd[MAXPATH + 1];
  char *renamefrom;
  int uid;
  int epsvall;
  int loggedin;
  int guest;
  int restartat;
  int debug;
  int idletime;
  int passive;
  int dataport;
  int type;
};

int login(struct ftpstate *fs, struct passwd *pw) {
  if (initgroups(pw->pw_name, pw->pw_gid) < 0) return -1;
  if (setgid(pw->pw_gid) < 0) return -1;
  if (setuid(pw->pw_uid) < 0) return -1;

  fs->uid = pw->pw_uid;
  strcpy(fs->wd, pw->pw_dir);

  return 0;
}

int convert(struct ftpstate *fs, char *filename, char *buf) {
  char buffer[2 * MAXPATH];
  char *p;

  if (*filename == '/' || *filename == '\\') {
    strcpy(buffer, filename);
  } else {
    strcpy(buffer, fs->wd);
    strcat(buffer, "/");
    strcat(buffer, filename);
  }

  if (realpath(buffer, buf) == NULL) return -1;

  p = buf;
  while (*p) {
    if (*p == '\\') *p = '/';
    p++;
  }

  return 0;
}

void addreply(struct ftpstate *fs, int code, const char *line, ...) {
  char buf[MAXPATH + 128];
  va_list ap;
  char *s;
  int l;
  struct reply *r;

  if (code) fs->replycode = code;
  va_start(ap, line);
  vsnprintf(buf, MAXPATH + 50, line, ap);
  va_end(ap);

  s = buf;
  while (*s) {
    char *e = s;
    while (*e && *e != '\n') e++;
    l = e - s;

    r = (struct reply *) malloc(sizeof(struct reply) + l);
    if (!r) return;
    memcpy(r->line, s, l);
    r->line[l] = 0;
    r->next = NULL;

    if (fs->lastreply) {
      fs->lastreply->next = r;
    } else {
      fs->firstreply = r;
    }

    fs->lastreply = r;

    s = e;
    if (*s) s++;
  }
}

void error(struct ftpstate *fs, int code, char *msg) {
  int err = errno;
  if (err == 0) {
    syslog(LOG_ERR, "%s", msg);
    addreply(fs, code, "%s", msg);
  } else {
    char *errmsg = strerror(errno);
    syslog(LOG_ERR, "%s: %s", msg, errmsg);
    addreply(fs, code, "%s: %s", msg, errmsg);
  }
}

void doreply(struct ftpstate *fs) {
  struct reply *r = fs->firstreply;
  while (r) {
    struct reply *next = r->next;
    fprintf(fs->out, "%03d %s\r\n", fs->replycode, r->line);
    if (logging > 1) syslog(LOG_DEBUG, "%03d %s", fs->replycode, r->line);
    free(r);
    r = next;
  }

  fs->firstreply = fs->lastreply = NULL;
  fflush(fs->out);
}

int opendata(struct ftpstate *fs) {
  struct sockaddr_in sin;
  int sock;
  int len;

  if (fs->datasock < 0) {
    addreply(fs, 425, "No data connection");
    return -1;
  }

  if (fs->passive) {
    fd_set rs;
    struct timeval tv;

    FD_ZERO(&rs);
    FD_SET(fs->datasock, &rs);
    tv.tv_sec = fs->idletime;
    tv.tv_usec = 0;
    if (select(fs->datasock + 1, &rs, NULL, NULL, &tv) < 0) {
      addreply(fs, 421, "timeout (no connection for %d seconds)", fs->idletime);
      return -1;
    }

    len = sizeof(sin);
    sock = accept(fs->datasock, (struct sockaddr *) &sin, &len);
    if (sock < 0) {
      error(fs, 421, "accept failed");
      close(fs->datasock);
      fs->datasock = -1;
      return -1;
    }

    if (!fs->guest && sin.sin_addr.s_addr != fs->peer.sin_addr.s_addr) {
      addreply(fs, 425, "Connection must originate at %s", inet_ntoa(fs->peer.sin_addr));
      close(sock);
      close(fs->datasock);
      fs->datasock = -1;
      return -1;
    }

    addreply(fs, 150, "Accepted data connection from %s:%d", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
  } else {
    sin.sin_addr.s_addr = fs->peer.sin_addr.s_addr;
    sin.sin_port = htons(fs->dataport);
    sin.sin_family = AF_INET;

    if (connect(fs->datasock, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
      addreply(fs, 425, "Could not open data connection to %s port %d: %s", inet_ntoa(sin.sin_addr), fs->dataport, strerror(errno));
      close(fs->datasock);
      fs->datasock = -1;
      return -1;
    }

    sock = fs->datasock;
    fs->datasock = -1;
    addreply(fs, 150, "Connecting to %s:%d", inet_ntoa(sin.sin_addr), fs->dataport);
  }

  return sock;
}

void douser(struct ftpstate *fs, char *username) {
  struct passwd *pw;

  if (fs->loggedin) {
    if (username) {
      if (!fs->guest) {
        addreply(fs, 530, "You're already logged in.");
      } else {
        addreply(fs, 230, "Anonymous user logged in.");
      }
    }
    return;
  }

  if (username && strcmp(username, "ftp") != 0 && strcmp(username, "anonymous") != 0) {
    pw = getpwnam(username);
    if (pw == NULL) {
      addreply(fs, 331, "User %s unknown.", username);
    } else {
      fs->uid = pw->pw_uid;
      addreply(fs, 331, "User %s OK.  Password required.", pw->pw_name);
    }
    
    fs->loggedin = 0;
  } else {
    pw = getpwnam("ftp");
    if (!pw) {
      addreply(fs, 530, "Anonymous access not allowed.");
    } else {
      if (login(fs, pw) < 0) {
        addreply(fs, 530, "Could not login anonymous user.");
      } else {
        addreply(fs, 230, "Anonymous user logged in.");
        fs->loggedin = fs->guest = 1;
        syslog(LOG_INFO, "guest logged in");
      }
    }
  }
}

void dopass(struct ftpstate *fs, char *password) {
  struct passwd *pw;

  if (fs->uid < 0) {
    addreply(fs, 332, "Need account for login.");
  } else if ((pw = getpwuid(fs->uid)) == NULL) {
    addreply(fs, 331, "Unknow user.");
  } else if (strcmp(pw->pw_passwd, crypt(password, pw->pw_passwd)) == 0) {
    if (login(fs, pw) < 0) {
      addreply(fs, 530, "Could not login user.");
    } else {
      fs->loggedin = 1;
      addreply(fs, 230, "OK. Current directory is %s", fs->wd);
      syslog(LOG_INFO, "%s logged in", pw->pw_name);
    }
  } else {
    addreply(fs, 530, "Sorry");
  }
}

void docwd(struct ftpstate *fs, char *dir) {
  char newwd[MAXPATH];
  struct stat st;

  if (convert(fs, dir, newwd) < 0 || stat(newwd, &st) < 0) {
    addreply(fs, 530, "Cannot change directory to %s: %s", dir, strerror(errno));
    return;
  }

  if (!S_ISDIR(st.st_mode)) {
    addreply(fs, 530, "Not a directory");
    return;
  }

  strcpy(fs->wd, newwd);
  addreply(fs, 250, "Changed to %s", fs->wd);
}

void doretr(struct ftpstate *fs, char *name) {
  char filename[MAXPATH];
  int f;
  int sock;
  struct stat st;
  clock_t started;
  clock_t ended;
  int ofs;
  int n;
  char buf[4096];
  double t;
  double speed;

  if (convert(fs, name, filename) < 0) {
    error(fs, 550, name);
    return;
  }

  f = open(filename, O_RDONLY);
  if (f < 0) {
    char buffer[MAXPATH + 40];
    snprintf(buffer, MAXPATH + 39, "Can't open %s", name);
    error(fs, 550, buffer);
    return;
  }

  if (fstat(f, &st)) {
    close(f);
    error(fs, 451, "can't find file size");
    return;
  }

  if (fs->restartat && fs->restartat > st.st_size) {
    addreply(fs, 451, "Restart offset %d is too large for file size %d.\nRestart offset reset to 0.", fs->restartat, st.st_size);
    return;
  }

  if (!S_ISREG(st.st_mode)) {
    close(f);
    addreply(fs, 450, "Not a regular file");
    return;
  }

  sock = opendata(fs);
  if (sock < 0) {
    close(f);
    return;
  }

  if (fs->restartat == st.st_size) {
    close(f);
    close(sock);
    addreply(fs, 226, "Nothing left to download.  Restart offset reset to 0.");
    return;
  }

  //if (fs->type == 1) addreply(fs, 0, "NOTE: ASCII mode requested, but binary mode used");
  //if (st.st_size - fs->restartat > 4096) addreply(fs, 0, "%.1f kbytes to download", (st.st_size - fs->restartat) / 1024.0);

  doreply(fs);

  started = clock();
  ofs = fs->restartat;
  if (ofs != 0) lseek(f, ofs, SEEK_SET);

  while (ofs < st.st_size) {
    n = st.st_size - ofs;
    if (n > sizeof(buf)) n = sizeof(buf);

    n  = read(f, buf, n);
    if (n <= 0) {
      if (n == 0) {
        addreply(fs, 451, "unexpected end of file");
      } else {
        error(fs, 451, "error reading file");
      }

      close(f);
      close(sock);
      return;
    }

    if (send(sock, buf, n, 0) < 0) {
      addreply(fs, 426, "Transfer aborted");
      close(f);
      close(sock);
      return;
    }

    ofs += n;
  }

  ended = clock();

  t = (ended - started) / 1000.0;
  addreply(fs, 226, "File written successfully");

  if (t != 0.0 && st.st_size - fs->restartat > 0) {
    speed = (st.st_size - fs->restartat) / t;
  } else {
    speed = 0.0;
  }

  //addreply(fs, 0, "%.3f seconds (measured by the server), %.2f %sb/s", t, speed > 524288 ? speed / 1048576 : speed / 1024, speed > 524288 ? "M" : "K");

  close(f);
  close(sock);

  if (fs->restartat != 0) {
    fs->restartat = 0;
    addreply(fs, 0, "Restart offset reset to 0.");
  }
}

void dorest(struct ftpstate *fs, char *name) {
  char *endptr;

  fs->restartat = strtoul(name, &endptr, 10);
  if (*endptr) {
    fs->restartat = 0;
    addreply(fs, 501, "RESTART needs numeric parameter.\nRestart offset set to 0.");
  } else {
    syslog(LOG_NOTICE, "info: restart %d", fs->restartat);
    addreply(fs, 350, "Restarting at %ld. Send STOR or RETR to initiate transfer.", fs->restartat);
  }
}

void dodele(struct ftpstate *fs, char *name) {
  char filename[MAXPATH];

  if (convert(fs, name, filename) < 0) {
    error(fs, 550, name);
    return;
  }

  if (fs->guest) {
    addreply(fs, 550, "Anonymous users can not delete files.");
  } else if (unlink(filename) < 0) {
    addreply(fs, 550, "Could not delete %s: %s", name, strerror(errno));
  } else {
    addreply(fs, 250, "Deleted %s", name);
  }
}

void dostor(struct ftpstate *fs, char *name) {
  char filename[MAXPATH];
  struct stat st;
  int f;
  int sock;
  char buf[4096];
  int n;

  if (convert(fs, name, filename) < 0) {
    error(fs, 550, name);
    return;
  }

  if (fs->type < 1) {
    addreply(fs, 503, "Only ASCII and binary modes are supported");
    return;
  }

  if (stat(filename, &st) >= 0) {
    if (fs->guest) {
      addreply(fs, 553, "Anonymous users may not overwrite existing files");
      return;
    }
  } else if (errno != ENOENT) {
    error(fs, 553, "Can't check for file presence");
    return;
  }

  f = open(filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
  if (f < 0) {
    error(fs, 553, "Can't open file");
    return;
  }

  if (fs->restartat && lseek(f, fs->restartat, SEEK_SET) < 0) {
    close(f);
    error(fs, 451, "can't seek");
    return;
  }

  sock = opendata(fs);
  if (sock < 0) {
    close(f);
    return;
  }
  doreply(fs);

  while (1) {
    n = recv(sock, buf, sizeof(buf), 0);
    if (n < 0) {
      error(fs, 451, "Error during read from data connection");
      close(f);
      close(sock);
      addreply(fs, 451, "%s %s", name, unlink(filename) ? "partially uploaded" : "removed");
      return;
    }

    if (n == 0) break;

    if (write(f, buf, n) < 0) {
      error(fs, 450, "Error during write to file");
      close(f);
      close(sock);
      addreply(fs, 450, "%s %s", name, unlink(filename) ? "partially uploaded" : "removed");
      return;
    }
  }

  fchmod(f, 0644);
  addreply(fs, 226, "File written successfully");

  close(f);
  close(sock);

  if (fs->restartat) {
    fs->restartat = 0;
    addreply(fs, 0, "Restart offset reset to 0.");
  }
}

void domkd(struct ftpstate *fs, char *name) {
  char filename[MAXPATH];

  if (convert(fs, name, filename) < 0) {
    error(fs, 550, name);
    return;
  }

  if (fs->guest) {
    addreply(fs, 550, "Sorry, anonymous users are not allowed to make directories.");
  } else if (mkdir(filename, 0755) < 0) {
    error(fs, 550, "Can't create directory");
  } else {
    addreply(fs, 257, "MKD command successful.");
  }
}

void dormd(struct ftpstate *fs, char *name) {
  char filename[MAXPATH];

  if (convert(fs, name, filename) < 0) {
    error(fs, 550, name);
    return;
  }

  if (fs->guest) {
    addreply(fs, 550, "Sorry, anonymous users are not allowed to remove directories.");
  } else if (rmdir(filename) < 0) {
    error(fs, 550, "Can't remove directory");
  } else {
    addreply(fs, 250, "RMD command successful.");
  }
}

void domdtm(struct ftpstate *fs, char *name) {
  char filename[MAXPATH];
  struct stat st;
  struct tm *t;

  if (convert(fs, name, filename) < 0) {
    error(fs, 550, name);
    return;
  }

  if (stat(filename, &st) < 0) {
    error(fs, 550, "Unable to stat()");
  } else if (!S_ISREG(st.st_mode)) {
    addreply(fs, 550, "Not a regular file");
  } else {
    t = gmtime(& st.st_mtime);
    if (!t) {
      addreply(fs, 550, "gmtime() returned NULL");
    } else {
      addreply(fs, 213, "%04d%02d%02d%02d%02d%02d", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
    }
  }
}

void dosize(struct ftpstate *fs, char *name) {
  char filename[MAXPATH];
  struct stat st;

  if (convert(fs, name, filename) < 0) {
    error(fs, 550, name);
    return;
  }

  if (stat(filename, &st ) < 0) {
    addreply(fs, 550, "Unable to stat()");
  } else if (!S_ISREG(st.st_mode)) {
    addreply(fs, 550, "Not a regular file");
  } else {
    addreply(fs, 213, "%ld", st.st_size);
  }
}

void doport(struct ftpstate *fs, unsigned int ip, unsigned int port) {
  struct sockaddr_in sin;

  if (fs->datasock != -1) {
    close(fs->datasock);
    fs->datasock = -1;
  }

  fs->datasock = socket(AF_INET, SOCK_STREAM, 0);
  if (fs->datasock < 0) {
    error(fs, 425, "Can't make data socket");
    return;
  }

  //int on = 1;
  //if (setsockopt(fs->datasock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
  //  error(fs, 421, "setsockopt");
  //  return;
  //}

  sin.sin_family = AF_INET;
  sin.sin_addr.s_addr = INADDR_ANY;
  sin.sin_port = htons(20); // FTP data connection port
  if (bind(fs->datasock, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
    error(fs, 220, "bind");
    close(fs->datasock);
    fs->datasock = -1;
    return;
  }

  if (fs->debug) addreply(fs, 0, "My data connection endpoint is %s:%d", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));

  fs->dataport = port;

  if (htonl(ip) != fs->peer.sin_addr.s_addr) {
    addreply(fs, 425, "Will not open connection to %d.%d.%d.%d (only to %s)",
             (ip >> 24) & 255, (ip >> 16) & 255, (ip >> 8) & 255, ip & 255,
             inet_ntoa(fs->peer.sin_addr));
    close(fs->datasock);
    fs->datasock = -1;
    return;
  }

  fs->passive = 0;

  addreply(fs, 200, "PORT command successful");
}

void dopasv(struct ftpstate *fs, int useepsv) {
  unsigned int a;
  unsigned int p;
  struct sockaddr_in sin;
  unsigned int len;

  if (fs->datasock != -1) {
    close(fs->datasock);
    fs->datasock = -1;
  }

  len = sizeof(sin);
  if (getsockname(fs->ctrlsock, (struct sockaddr *) &sin, &len) < 0) {
    error(fs, 425, "Can't getsockname");
    return;
  }

  fs->datasock = socket(AF_INET, SOCK_STREAM, 0);
  if (fs->datasock < 0) {
    error(fs, 425, "Can't open passive connection");
    return;
  }

  sin.sin_port = 0;
  if (bind(fs->datasock, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
    error(fs, 425, "Can't bind to socket");
    return;
  }

  len = sizeof(sin);
  if (getsockname(fs->datasock, (struct sockaddr *) &sin, &len) < 0) {
    error(fs, 425, "Can't getsockname");
    return;
  }

  listen(fs->datasock, 1);

  a = ntohl(sin.sin_addr.s_addr);
  p = ntohs(sin.sin_port);
  if (useepsv) {
    addreply(fs, 229, "Extended Passive mode OK (|||%d|)", p);
  } else {
    addreply(fs, 227, "Passive mode OK (%d,%d,%d,%d,%d,%d)", (a >> 24) & 255, (a >> 16) & 255, (a >> 8) & 255, a & 255, (p >> 8) & 255, p & 255);
  }
  
  fs->passive = 1;
}

void domode(struct ftpstate *fs, char *arg) {
  if (!arg || !*arg) {
    addreply(fs, 500, "No arguments\nNot that it matters, only MODE S is supported");
  } else if (strcmp(arg, "S") != 0) {
    addreply(fs, 504, "MODE %s is not supported\nOnly S(tream) is supported", arg);
  } else {
    addreply(fs, 200, "S OK");
  }
}

void dostru(struct ftpstate *fs, char *arg) {
  if (!arg || !*arg) {
    addreply(fs, 500, "No arguments\nNot that it matters, only STRU F is supported");
  } else if (strcmp(arg, "F") != 0) {
    addreply(fs, 504, "STRU %s is not supported\nOnly F(ile) is supported", arg);
  } else {
    addreply(fs, 200, "F OK");
  }
}

void dotype(struct ftpstate *fs, char *arg) {
  fs->replycode = 200;

  if (!arg || !*arg) {
    addreply(fs, 501, "TYPE needs an argument\nOnly A(scii), I(mage) and L(ocal) are supported");
  } else if (tolower(*arg) == 'a') {
    fs->type = 1;
  } else if (tolower(*arg) == 'i') {
    fs->type = 2;
  } else if (tolower(*arg) == 'l') {
    if (arg[1] == '8') {
      fs->type = 2;
    } else if (isdigit(arg[1])) {
      addreply(fs, 504, "Only 8-bit bytes are supported");
    } else {
      addreply(fs, 0, "Byte size not specified");
      fs->type = 2;
    }
  } else {
    addreply(fs, 504, "Unknown TYPE: %s", arg);
  }

  addreply(fs, 0, "TYPE is now %s", (fs->type > 1) ? "8-bit binary" : "ASCII");
}

void dornfr(struct ftpstate *fs, char *name) {
  char filename[MAXPATH];
  struct stat st;

  if (convert(fs, name, filename) < 0) {
    error(fs, 550, name);
    return;
  }

  if (fs->guest) {
    addreply(fs, 550, "Sorry, anonymous users are not allowed to rename files.");
  } else if (stat(filename, &st) < 0) {
    addreply(fs, 550, "File does not exist");
  } else {
    if (fs->renamefrom) {
      addreply(fs, 0, "Aborting previous rename operation.");
      free(fs->renamefrom);
    }
    fs->renamefrom = strdup(filename);
    addreply(fs, 350, "RNFR accepted - file exists, ready for destination.");
  }
}

void dornto(struct ftpstate *fs, char *name) {
  char filename[MAXPATH];
  struct stat st;

  if (convert(fs, name, filename) < 0) {
    error(fs, 550, name);
    return;
  }

  if (fs->guest) {
    addreply(fs, 550, "Sorry, anonymous users are not allowed to rename files.");
  } else if (stat(filename, &st) == 0) {
    addreply(fs, 550, "RENAME Failed - destination file already exists.");
  } else if (!fs->renamefrom) {
    addreply(fs, 503, "Need RNFR before RNTO");
  } else if (rename(fs->renamefrom, filename) < 0) {
    addreply(fs, 550, "Rename failed: %s", strerror(errno));
  } else {
    addreply(fs, 250, "File renamed.");
  }

  if (fs->renamefrom) free(fs->renamefrom);
  fs->renamefrom = NULL;
}

void donlist(struct ftpstate *fs, char *arg) {
  static char *months[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

  char dirname[MAXPATH];
  DIR *dir;
  struct dirent *de;
  int sock;
  int matches;
  int opt_l = 0;
  time_t now = time(NULL);

  while (isspace(*arg)) arg++;

  while (*arg == '-') {
    while (isalnum(*++arg)) {
      switch (*arg) {
        case 'l':
        case 'a':
          opt_l = 1;
          break;
      }
    }

    while (isspace(*arg)) arg++;
  }

  if (convert(fs, arg, dirname) < 0) {
    error(fs, 550, arg);
    return;
  }

  dir = opendir(dirname);
  if (!dir) {
    error(fs, 550, dirname);
    return;
  }

  sock = opendata(fs);
  if (sock < 0) {
    closedir(dir);
    return;
  }
  doreply(fs);

  matches = 0;
  while ((de = readdir(dir)) != NULL) {
    char buf[MAXPATH + 128];

    if (opt_l) {
      char fn[MAXPATH * 2 + 2];
      struct stat st;
      char perm[11];
      char timestr[6];
      struct passwd *pwd;
      struct group *grp;
      struct tm *tm;

      strcpy(fn, dirname);
      strcat(fn, "/");
      strcat(fn, de->d_name);

      if (stat(fn, &st) < 0) continue;
      if ((tm = localtime(&st.st_mtime)) == NULL) continue;

      strcpy(perm, " ---------");
      switch (st.st_mode & S_IFMT) {
        case S_IFREG: perm[0] = '-'; break;
        case S_IFLNK: perm[0] = 'l'; break;
        case S_IFDIR: perm[0] = 'd'; break;
        case S_IFBLK: perm[0] = 'b'; break;
        case S_IFCHR: perm[0] = 'c'; break;
        case S_IFPKT: perm[0] = 'p'; break;
      }

      if (st.st_mode & 0400) perm[1] = 'r';
      if (st.st_mode & 0200) perm[2] = 'w';
      if (st.st_mode & 0100) perm[3] = 'x';
      if (st.st_mode & 0040) perm[4] = 'r';
      if (st.st_mode & 0020) perm[5] = 'w';
      if (st.st_mode & 0010) perm[6] = 'x';
      if (st.st_mode & 0004) perm[7] = 'r';
      if (st.st_mode & 0002) perm[8] = 'w';
      if (st.st_mode & 0001) perm[9] = 'x';

      pwd = getpwuid(st.st_uid);
      grp = getgrgid(st.st_gid);
      if (!pwd || !grp) continue;

      if (now - st.st_mtime > 180 * 24 * 60 * 60) {
        snprintf(timestr, 6, "%5d", tm->tm_year + 1900);
      } else {
        snprintf(timestr, 6, "%02d:%02d", tm->tm_hour, tm->tm_min);
      }

      snprintf(buf, sizeof(buf), "%s %3d %s %s %7d %s %2d %s %s\r\n",
               perm, st.st_nlink, pwd->pw_name, grp->gr_name,
               st.st_size, months[tm->tm_mon], tm->tm_mday, timestr, de->d_name);
    } else {
      sprintf(buf, "%s\r\n", de->d_name);
    }
    
    send(sock, buf, strlen(buf), 0);
    matches++;
  }

  addreply(fs, 226, "%d matches total", matches);

  close(sock);
  closedir(dir);
}

int docmd(struct ftpstate *fs) {
  char *arg;
  char *cmd;
  int cmdsize;
  int n = 0;

  if (!fgets(fs->cmd, sizeof(fs->cmd), fs->in)) {
    if (errno == ETIMEOUT) addreply(fs, 421, "Timeout (%d seconds).", fs->idletime / 1000);
    return -1;
  }
  cmd = fs->cmd;
  cmdsize = strlen(cmd);

  if (fs->debug) addreply(fs, 0, "%s", cmd);

  n = 0;
  while (isalpha(cmd[n]) && n < cmdsize) {
    cmd[n] = tolower(cmd[n]);
    n++;
  }

  if (!n) {
    addreply(fs, 221, "Goodbye.");
    return 0;
  }

  while (isspace(cmd[n]) && n < cmdsize) cmd[n++] = '\0';
  arg = cmd + n;

  while (cmd[n] && n < cmdsize) n++;
  n--;

  while (isspace(cmd[n])) cmd[n--] = '\0';

  if (logging > 0) syslog(LOG_DEBUG, "CMD %s %s", cmd, strcmp(cmd, "pass") ? arg : "<password>");

  if (strlen(cmd) > 10) {
    addreply(fs, 500, "Unknown command.");
  } else if (strlen(arg) >= MAXPATH) { // ">=" on purpose.
    addreply(fs, 501, "Cannot handle %d-character file names", strlen(arg));
  } else if (strcmp(cmd, "user") == 0) {
    douser(fs, arg);
  } else if (strcmp(cmd, "pass") == 0) {
    dopass(fs, arg);
  } else if (strcmp(cmd, "quit") == 0) {
     addreply(fs, 221, "Goodbye");
     return 0;
  } else if (strcmp(cmd, "noop") == 0) {
    addreply(fs, 200, "NOOP command successful");
  } else if (strcmp(cmd, "syst") == 0) {
    addreply(fs, 215, "UNIX Type: L8");
  } else if (strcmp(cmd, "feat") == 0) {
    addreply(fs, 500, "Unsupported command");
  } else if (strcmp(cmd, "port") == 0|| strcmp(cmd, "eprt") == 0) {
    // Don't auto-login for PORT or PASV, but do auto-login
    // for the command which uses the data connection
    unsigned int a1, a2, a3, a4, p1, p2;

    if (fs->epsvall) {
      addreply(fs, 501, "Cannot use PORT/EPRT after EPSV ALL");
    } else if (cmd[0] == 'e' && strncmp(arg, "|2|", 3) == 0) {
      addreply(fs, 522, "IPv6 not supported, use IPv4 (1)");
    } else if (cmd[0] == 'e' && 
                sscanf(arg, "|1|%u.%u.%u.%u|%u|", &a1, &a2, &a3, &a4, &p1) == 5  &&
                a1 < 256 && a2 < 256 && a3 < 256 && a4 < 256 && p1 < 65536) {
      doport(fs, (a1 << 24) + (a2 << 16) + (a3 << 8) + a4, p1);
    } else if (cmd[0] == 'p' &&
               sscanf(arg, "%u,%u,%u,%u,%u,%u", &a1, &a2, &a3, &a4, &p1, &p2) == 6 &&
               a1 < 256 && a2 < 256 && a3 < 256 && a4 < 256 && p1 < 256 && p2 < 256) {
      doport(fs, (a1 << 24) + (a2 << 16) + (a3 << 8) + a4, ((p1 << 8) + p2));
    } else {
      addreply(fs, 501, "Syntax error.");
    }
  } else if (strcmp(cmd, "pasv") == 0) {
    dopasv(fs, 0);
  } else if (strcmp(cmd, "epsv") == 0) {
    if (strcmp(arg, "all") == 0) {
      addreply(fs, 220, "OK; will reject non-EPSV data connections");
      fs->epsvall++;
    } else if (strcmp(arg, "2") == 0) {
      addreply(fs, 522, "IPv6 not supported, use IPv4 (1)");
    } else if (strlen(arg) == 0 || strcmp(arg, "1") == 0) {
      dopasv(fs, 1);
    }
  } else if (strcmp(cmd, "pwd") == 0 || strcmp(cmd, "xpwd") == 0) {
    if (fs->loggedin) {
      addreply(fs, 257, "\"%s\"", fs->wd);
    } else {
      addreply(fs, 550, "Not logged in");
    }
  } else if (strcmp(cmd, "auth") == 0) {
    // RFC 2228 Page 5 Authentication/Security mechanism (AUTH)
    addreply(fs, 502, "Security extensions not implemented");
  } else {
    // From this point, all commands trigger an automatic login
    douser(fs, NULL);

    if (strcmp(cmd, "cwd") == 0) {
      docwd(fs, arg);
    } else if (strcmp(cmd, "cdup") == 0) {
      docwd(fs, "..");
    } else if (strcmp(cmd, "retr") == 0) {
      if (arg && *arg) {
        doretr(fs, arg);
      } else {
        addreply(fs, 501, "No file name");
      }
    } else if (strcmp(cmd, "rest") == 0) {
      if (arg && *arg) {
        dorest(fs, arg);
      } else {
        addreply(fs, 501, "No restart point");
      }
    } else if (strcmp(cmd, "dele") == 0) {
      if (arg && *arg) {
        dodele(fs, arg);
      } else {
        addreply(fs, 501, "No file name");
      }
    } else if (strcmp(cmd, "stor") == 0) {
      if (arg && *arg) {
        dostor(fs, arg);
      } else {
        addreply(fs, 501, "No file name");
      }
    } else if (strcmp(cmd, "mkd") == 0 || strcmp(cmd, "xmkd") == 0) {
      if (arg && *arg) {
        domkd(fs, arg);
      } else {
        addreply(fs, 501, "No directory name");
      }
    } else if (strcmp(cmd, "rmd") == 0 || strcmp(cmd, "xrmd") == 0) {
      if (arg && *arg) {
        dormd(fs, arg);
      } else {
        addreply(fs, 550, "No directory name");
      }
    } else if (strcmp(cmd, "list") == 0 || strcmp(cmd, "nlst") == 0) {
      donlist(fs, (arg && *arg) ? arg : "-l");
    } else if (strcmp(cmd, "type") == 0) {
      dotype(fs, arg);
    } else if (strcmp(cmd, "mode") == 0) {
      domode(fs, arg);
    } else if (strcmp(cmd, "stru") == 0) {
      dostru(fs, arg);
    } else if (strcmp(cmd, "abor") == 0) {
      addreply(fs, 226, "ABOR succeeded.");
    } else if (strcmp(cmd, "site") == 0) {
      char *sitearg;

      sitearg = arg;
      while (sitearg && *sitearg && !isspace(*sitearg)) sitearg++;
      if (sitearg) *sitearg++ = '\0';

      if (strcmp(arg, "idle") == 0) {
        if (!*sitearg) {
          addreply(fs, 501, "SITE IDLE: need argument");
        } else {
          unsigned long int i = 0;

          i = strtoul(sitearg, &sitearg, 10);
          if (sitearg && *sitearg) {
            addreply(fs, 501, "Garbage (%s) after value (%u)", sitearg, i);
          } else {
            if (i > 7200) i = 7200;
            if (i < 10) i = 10;
            fs->idletime = i * 1000;
            setsockopt(fs->ctrlsock, SOL_SOCKET, SO_RCVTIMEO, &fs->idletime, 4);
            setsockopt(fs->ctrlsock, SOL_SOCKET, SO_SNDTIMEO, &fs->idletime, 4);

            addreply(fs, 200, "Idle time set to %u seconds", fs->idletime / 1000);
          }
        }
      } else if (arg && *arg) {
        addreply(fs, 500, "SITE %s unknown", arg);
      } else {
        addreply(fs, 500, "SITE: argument needed");
      }
    } else if (strcmp(cmd, "xdbg") == 0) {
      fs->debug++;
      addreply(fs, 200, "XDBG command succeeded, debug level is now %d.", fs->debug);
    } else if (strcmp(cmd, "mdtm") == 0) {
      domdtm(fs, (arg && *arg) ? arg :  "");
    } else if (strcmp(cmd, "size") == 0) {
      dosize(fs, (arg && *arg) ? arg :  "");
    } else if (strcmp(cmd, "rnfr") == 0) {
      if (arg && *arg) {
        dornfr(fs, arg);
      } else {
        addreply(fs, 550, "No file name given.");
      }
    } else if (strcmp(cmd, "rnto") == 0) {
      if (arg && *arg) {
        dornto(fs, arg);
      } else {
        addreply(fs, 550, "No file name given.");
      }
    } else if (strcmp(cmd, "rest") == 0) {
      fs->restartat = 0;
    } else {
      addreply(fs, 500, "Unknown command.");
    }
  }

  return 1;
}

void __stdcall ftp_task(void *arg) {
  struct ftpstate ftpstate;
  struct ftpstate *fs = &ftpstate;
  struct process *proc = gettib()->proc;
  int s = (int) arg;
  int len;

  // Initialize client state
  memset(fs, 0, sizeof(struct ftpstate));
  fs->ctrlsock = s;
  fs->datasock = -1;
  fs->uid = -1;

  len = sizeof(fs->peer);
  getpeername(s, (struct sockaddr *) &fs->peer, &len);

  // Set process identifer
  if (!proc->ident && !proc->cmdline) {
    struct sockaddr_in sin;
    int sinlen = sizeof sin;

    getpeername(s, (struct sockaddr *) &sin, &sinlen);
    proc->ident = strdup("ftp");
    proc->cmdline = strdup(inet_ntoa(sin.sin_addr));
  }

  // Set idle timeout for connection
  setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
  setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
  fs->idletime = timeout;

  // Open data input and out file descriptors
  fs->in = fdopen(s, "r");
  if (!fs->in) {
    close(s);
    return;
  }

  fs->out = fdopen(dup(s), "w");
  if (!fs->out) {
    fclose(fs->in);
    return;
  }

  // Handle commands
  addreply(fs, 220, "FTP server ready");
  while (1) {
    doreply(fs);
    if (docmd(fs) <= 0) break;
  }
  doreply(fs);

  // Close connection
  if (fs->renamefrom) free(fs->renamefrom);
  if (fs->in) fclose(fs->in);
  if (fs->out) fclose(fs->out);
  if (fs->datasock != -1) close(fs->datasock);
}

int main(int argc, char *argv[]) {
  int s;
  int rc;
  struct sockaddr_in sin;
  int hthread;

  if (argc == 2) {
    if (strcmp(argv[1], "start") == 0) {
      char path[MAXPATH];
      
      getmodpath(NULL, path, MAXPATH);
      hthread = spawn(P_NOWAIT | P_DETACH, path, "", NULL, NULL);
      if (hthread < 0) syslog(LOG_ERR, "error %d (%s) in spawn", errno, strerror(errno));
      close(hthread);
      return 0;
    } else if (strcmp(argv[1], "stop") == 0) {
      state = STOPPED;
      close(sock);
      return 0;
    }
  }

  port = get_numeric_property(osconfig(), "ftpd", "port", getservbyname("ftp", "tcp")->s_port);

  sock = socket(AF_INET, SOCK_STREAM, 0);
  if (sock < 0) {
    syslog(LOG_ERR, "error %d (%s) in socket", errno, strerror(errno));
    return 1;
  }

  sin.sin_family = AF_INET;
  sin.sin_addr.s_addr = INADDR_ANY;
  sin.sin_port = htons(port);
  rc = bind(sock, (struct sockaddr *) &sin, sizeof sin);
  if (rc < 0) {
    syslog(LOG_ERR, "error %d (%s) in bind", errno, strerror(errno));
    return 1;
  }

  rc = listen(sock, 5);
  if (rc < 0) {
    syslog(LOG_ERR, "error %d (%s) in listen", errno, strerror(errno));
    return 1;
  }

  syslog(LOG_INFO, "ftpd started on port %d", port);
  state = RUNNING;
  while (1) {
    struct sockaddr_in sin;

    s = accept(sock, (struct sockaddr *) &sin, NULL);
    if (state == STOPPED) break;
    if (s < 0) {
      syslog(LOG_ERR, "error %d (%s) in accept", errno, strerror(errno));
      return 1;
    }

    syslog(LOG_INFO, "FTP client connected from %a", &sin.sin_addr);

    hthread = beginthread(ftp_task, 0, (void *) s, CREATE_NEW_PROCESS | CREATE_DETACHED, "ftp", NULL);
    close(hthread);
  }

  syslog(LOG_INFO, "ftpd stopped");
}