freebsd-src/libexec/ftpd/ftpd.c
Allan Jude e209715001 ftpd: stop using -g flag for /bin/ls
In 3bfbb521 the behaviour of ls was changed such that -g was no longer
a noop for compatibility with BSD 4.3, but instead changed the output
of long mode to exclude the owner of the file and display only the
group.

Update how FTPd invokes ls to restore the previous behaviour

Reported-by:	Andrew Fengler <andrew.fengler@scaleengine.com>
Reviewed-by:	jrtc27, des, imp
MFC after:	3 days
Sponsored-by:	ScaleEngine Inc.
Fixes:		3bfbb521fe ("ls: Improve POSIX compatibility for -g and -n.")
2024-05-21 22:50:14 +00:00

3436 lines
77 KiB
C

/*-
* SPDX-License-Identifier: BSD-3-Clause
*
* Copyright (c) 1985, 1988, 1990, 1992, 1993, 1994
* The Regents of the University of California. All rights reserved.
*
* 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 University 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 REGENTS 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 REGENTS 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.
*/
/*
* FTP server.
*/
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#define FTP_NAMES
#include <arpa/ftp.h>
#include <arpa/inet.h>
#include <arpa/telnet.h>
#include <ctype.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <glob.h>
#include <limits.h>
#include <netdb.h>
#include <pwd.h>
#include <grp.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#include <libutil.h>
#ifdef LOGIN_CAP
#include <login_cap.h>
#endif
#ifdef USE_PAM
#include <security/pam_appl.h>
#endif
#include "blacklist_client.h"
#include "pathnames.h"
#include "extern.h"
#include <stdarg.h>
static char version[] = "Version 6.00LS";
#undef main
union sockunion ctrl_addr;
union sockunion data_source;
union sockunion data_dest;
union sockunion his_addr;
union sockunion pasv_addr;
int daemon_mode;
int data;
int dataport;
int hostinfo = 1; /* print host-specific info in messages */
int logged_in;
struct passwd *pw;
char *homedir;
int ftpdebug;
int timeout = 900; /* timeout after 15 minutes of inactivity */
int maxtimeout = 7200;/* don't allow idle time to be set beyond 2 hours */
int logging;
int restricted_data_ports = 1;
int paranoid = 1; /* be extra careful about security */
int anon_only = 0; /* Only anonymous ftp allowed */
int assumeutf8 = 0; /* Assume that server file names are in UTF-8 */
int guest;
int dochroot;
char *chrootdir;
int dowtmp = 1;
int stats;
int statfd = -1;
int type;
int form;
int stru; /* avoid C keyword */
int mode;
int usedefault = 1; /* for data transfers */
int pdata = -1; /* for passive mode */
int readonly = 0; /* Server is in readonly mode. */
int noepsv = 0; /* EPSV command is disabled. */
int noretr = 0; /* RETR command is disabled. */
int noguestretr = 0; /* RETR command is disabled for anon users. */
int noguestmkd = 0; /* MKD command is disabled for anon users. */
int noguestmod = 1; /* anon users may not modify existing files. */
int use_blacklist = 0;
off_t file_size;
off_t byte_count;
#if !defined(CMASK) || CMASK == 0
#undef CMASK
#define CMASK 027
#endif
int defumask = CMASK; /* default umask value */
char tmpline[7];
char *hostname;
int epsvall = 0;
#ifdef VIRTUAL_HOSTING
char *ftpuser;
static struct ftphost {
struct ftphost *next;
struct addrinfo *hostinfo;
char *hostname;
char *anonuser;
char *statfile;
char *welcome;
char *loginmsg;
} *thishost, *firsthost;
#endif
char remotehost[NI_MAXHOST];
char *ident = NULL;
static char wtmpid[20];
#ifdef USE_PAM
static int auth_pam(struct passwd**, const char*);
pam_handle_t *pamh = NULL;
#endif
char *pid_file = NULL; /* means default location to pidfile(3) */
/*
* Limit number of pathnames that glob can return.
* A limit of 0 indicates the number of pathnames is unlimited.
*/
#define MAXGLOBARGS 16384
#
/*
* Timeout intervals for retrying connections
* to hosts that don't accept PORT cmds. This
* is a kludge, but given the problems with TCP...
*/
#define SWAITMAX 90 /* wait at most 90 seconds */
#define SWAITINT 5 /* interval between retries */
int swaitmax = SWAITMAX;
int swaitint = SWAITINT;
#ifdef SETPROCTITLE
char proctitle[LINE_MAX]; /* initial part of title */
#endif /* SETPROCTITLE */
#define LOGCMD(cmd, file) logcmd((cmd), (file), NULL, -1)
#define LOGCMD2(cmd, file1, file2) logcmd((cmd), (file1), (file2), -1)
#define LOGBYTES(cmd, file, cnt) logcmd((cmd), (file), NULL, (cnt))
static volatile sig_atomic_t recvurg;
static int transflag; /* NB: for debugging only */
#define STARTXFER flagxfer(1)
#define ENDXFER flagxfer(0)
#define START_UNSAFE maskurg(1)
#define END_UNSAFE maskurg(0)
/* It's OK to put an `else' clause after this macro. */
#define CHECKOOB(action) \
if (recvurg) { \
recvurg = 0; \
if (myoob()) { \
ENDXFER; \
action; \
} \
}
#ifdef VIRTUAL_HOSTING
static void inithosts(int);
static void selecthost(union sockunion *);
#endif
static void ack(char *);
static void sigurg(int);
static void maskurg(int);
static void flagxfer(int);
static int myoob(void);
static int checkuser(char *, char *, int, char **, int *);
static FILE *dataconn(char *, off_t, char *);
static void dolog(struct sockaddr *);
static void end_login(void);
static FILE *getdatasock(char *);
static int guniquefd(char *, char **);
static void lostconn(int);
static void sigquit(int);
static int receive_data(FILE *, FILE *);
static int send_data(FILE *, FILE *, size_t, off_t, int);
static struct passwd *
sgetpwnam(char *);
static char *sgetsave(char *);
static void reapchild(int);
static void appendf(char **, char *, ...) __printflike(2, 3);
static void logcmd(char *, char *, char *, off_t);
static void logxfer(char *, off_t, time_t);
static char *doublequote(char *);
static int *socksetup(int, char *, const char *);
int
main(int argc, char *argv[], char **envp)
{
socklen_t addrlen;
int ch, on = 1, tos, s = STDIN_FILENO;
char *cp, line[LINE_MAX];
FILE *fd;
char *bindname = NULL;
const char *bindport = "ftp";
int family = AF_UNSPEC;
struct sigaction sa;
tzset(); /* in case no timezone database in ~ftp */
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
/*
* Prevent diagnostic messages from appearing on stderr.
* We run as a daemon or from inetd; in both cases, there's
* more reason in logging to syslog.
*/
(void) freopen(_PATH_DEVNULL, "w", stderr);
opterr = 0;
/*
* LOG_NDELAY sets up the logging connection immediately,
* necessary for anonymous ftp's that chroot and can't do it later.
*/
openlog("ftpd", LOG_PID | LOG_NDELAY, LOG_FTP);
while ((ch = getopt(argc, argv,
"468a:ABdDEhlmMoOp:P:rRSt:T:u:UvW")) != -1) {
switch (ch) {
case '4':
family = (family == AF_INET6) ? AF_UNSPEC : AF_INET;
break;
case '6':
family = (family == AF_INET) ? AF_UNSPEC : AF_INET6;
break;
case '8':
assumeutf8 = 1;
break;
case 'a':
bindname = optarg;
break;
case 'A':
anon_only = 1;
break;
case 'B':
#ifdef USE_BLACKLIST
use_blacklist = 1;
#else
syslog(LOG_WARNING, "not compiled with USE_BLACKLIST support");
#endif
break;
case 'd':
ftpdebug++;
break;
case 'D':
daemon_mode++;
break;
case 'E':
noepsv = 1;
break;
case 'h':
hostinfo = 0;
break;
case 'l':
logging++; /* > 1 == extra logging */
break;
case 'm':
noguestmod = 0;
break;
case 'M':
noguestmkd = 1;
break;
case 'o':
noretr = 1;
break;
case 'O':
noguestretr = 1;
break;
case 'p':
pid_file = optarg;
break;
case 'P':
bindport = optarg;
break;
case 'r':
readonly = 1;
break;
case 'R':
paranoid = 0;
break;
case 'S':
stats++;
break;
case 't':
timeout = atoi(optarg);
if (maxtimeout < timeout)
maxtimeout = timeout;
break;
case 'T':
maxtimeout = atoi(optarg);
if (timeout > maxtimeout)
timeout = maxtimeout;
break;
case 'u':
{
long val = 0;
val = strtol(optarg, &optarg, 8);
if (*optarg != '\0' || val < 0)
syslog(LOG_WARNING, "bad value for -u");
else
defumask = val;
break;
}
case 'U':
restricted_data_ports = 0;
break;
case 'v':
ftpdebug++;
break;
case 'W':
dowtmp = 0;
break;
default:
syslog(LOG_WARNING, "unknown flag -%c ignored", optopt);
break;
}
}
/* handle filesize limit gracefully */
sa.sa_handler = SIG_IGN;
(void)sigaction(SIGXFSZ, &sa, NULL);
if (daemon_mode) {
int *ctl_sock, fd, maxfd = -1, nfds, i;
fd_set defreadfds, readfds;
pid_t pid;
struct pidfh *pfh;
if ((pfh = pidfile_open(pid_file, 0600, &pid)) == NULL) {
if (errno == EEXIST) {
syslog(LOG_ERR, "%s already running, pid %d",
getprogname(), (int)pid);
exit(1);
}
syslog(LOG_WARNING, "pidfile_open: %m");
}
/*
* Detach from parent.
*/
if (daemon(1, 1) < 0) {
syslog(LOG_ERR, "failed to become a daemon");
exit(1);
}
if (pfh != NULL && pidfile_write(pfh) == -1)
syslog(LOG_WARNING, "pidfile_write: %m");
sa.sa_handler = reapchild;
(void)sigaction(SIGCHLD, &sa, NULL);
#ifdef VIRTUAL_HOSTING
inithosts(family);
#endif
/*
* Open a socket, bind it to the FTP port, and start
* listening.
*/
ctl_sock = socksetup(family, bindname, bindport);
if (ctl_sock == NULL)
exit(1);
FD_ZERO(&defreadfds);
for (i = 1; i <= *ctl_sock; i++) {
FD_SET(ctl_sock[i], &defreadfds);
if (listen(ctl_sock[i], 32) < 0) {
syslog(LOG_ERR, "control listen: %m");
exit(1);
}
if (maxfd < ctl_sock[i])
maxfd = ctl_sock[i];
}
/*
* Loop forever accepting connection requests and forking off
* children to handle them.
*/
while (1) {
FD_COPY(&defreadfds, &readfds);
nfds = select(maxfd + 1, &readfds, NULL, NULL, 0);
if (nfds <= 0) {
if (nfds < 0 && errno != EINTR)
syslog(LOG_WARNING, "select: %m");
continue;
}
pid = -1;
for (i = 1; i <= *ctl_sock; i++)
if (FD_ISSET(ctl_sock[i], &readfds)) {
addrlen = sizeof(his_addr);
fd = accept(ctl_sock[i],
(struct sockaddr *)&his_addr,
&addrlen);
if (fd == -1) {
syslog(LOG_WARNING,
"accept: %m");
continue;
}
switch (pid = fork()) {
case 0:
/* child */
(void) dup2(fd, s);
(void) dup2(fd, STDOUT_FILENO);
(void) close(fd);
for (i = 1; i <= *ctl_sock; i++)
close(ctl_sock[i]);
if (pfh != NULL)
pidfile_close(pfh);
goto gotchild;
case -1:
syslog(LOG_WARNING, "fork: %m");
/* FALLTHROUGH */
default:
close(fd);
}
}
}
} else {
addrlen = sizeof(his_addr);
if (getpeername(s, (struct sockaddr *)&his_addr, &addrlen) < 0) {
syslog(LOG_ERR, "getpeername (%s): %m",argv[0]);
exit(1);
}
#ifdef VIRTUAL_HOSTING
if (his_addr.su_family == AF_INET6 &&
IN6_IS_ADDR_V4MAPPED(&his_addr.su_sin6.sin6_addr))
family = AF_INET;
else
family = his_addr.su_family;
inithosts(family);
#endif
}
gotchild:
sa.sa_handler = SIG_DFL;
(void)sigaction(SIGCHLD, &sa, NULL);
sa.sa_handler = sigurg;
sa.sa_flags = 0; /* don't restart syscalls for SIGURG */
(void)sigaction(SIGURG, &sa, NULL);
sigfillset(&sa.sa_mask); /* block all signals in handler */
sa.sa_flags = SA_RESTART;
sa.sa_handler = sigquit;
(void)sigaction(SIGHUP, &sa, NULL);
(void)sigaction(SIGINT, &sa, NULL);
(void)sigaction(SIGQUIT, &sa, NULL);
(void)sigaction(SIGTERM, &sa, NULL);
sa.sa_handler = lostconn;
(void)sigaction(SIGPIPE, &sa, NULL);
addrlen = sizeof(ctrl_addr);
if (getsockname(s, (struct sockaddr *)&ctrl_addr, &addrlen) < 0) {
syslog(LOG_ERR, "getsockname (%s): %m",argv[0]);
exit(1);
}
dataport = ntohs(ctrl_addr.su_port) - 1; /* as per RFC 959 */
#ifdef VIRTUAL_HOSTING
/* select our identity from virtual host table */
selecthost(&ctrl_addr);
#endif
#ifdef IP_TOS
if (ctrl_addr.su_family == AF_INET)
{
tos = IPTOS_LOWDELAY;
if (setsockopt(s, IPPROTO_IP, IP_TOS, &tos, sizeof(int)) < 0)
syslog(LOG_WARNING, "control setsockopt (IP_TOS): %m");
}
#endif
/*
* Disable Nagle on the control channel so that we don't have to wait
* for peer's ACK before issuing our next reply.
*/
if (setsockopt(s, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) < 0)
syslog(LOG_WARNING, "control setsockopt (TCP_NODELAY): %m");
data_source.su_port = htons(ntohs(ctrl_addr.su_port) - 1);
(void)snprintf(wtmpid, sizeof(wtmpid), "%xftpd", getpid());
/* Try to handle urgent data inline */
#ifdef SO_OOBINLINE
if (setsockopt(s, SOL_SOCKET, SO_OOBINLINE, &on, sizeof(on)) < 0)
syslog(LOG_WARNING, "control setsockopt (SO_OOBINLINE): %m");
#endif
#ifdef F_SETOWN
if (fcntl(s, F_SETOWN, getpid()) == -1)
syslog(LOG_ERR, "fcntl F_SETOWN: %m");
#endif
dolog((struct sockaddr *)&his_addr);
/*
* Set up default state
*/
data = -1;
type = TYPE_A;
form = FORM_N;
stru = STRU_F;
mode = MODE_S;
tmpline[0] = '\0';
/* If logins are disabled, print out the message. */
if ((fd = fopen(_PATH_NOLOGIN,"r")) != NULL) {
while (fgets(line, sizeof(line), fd) != NULL) {
if ((cp = strchr(line, '\n')) != NULL)
*cp = '\0';
lreply(530, "%s", line);
}
(void) fflush(stdout);
(void) fclose(fd);
reply(530, "System not available.");
exit(0);
}
#ifdef VIRTUAL_HOSTING
fd = fopen(thishost->welcome, "r");
#else
fd = fopen(_PATH_FTPWELCOME, "r");
#endif
if (fd != NULL) {
while (fgets(line, sizeof(line), fd) != NULL) {
if ((cp = strchr(line, '\n')) != NULL)
*cp = '\0';
lreply(220, "%s", line);
}
(void) fflush(stdout);
(void) fclose(fd);
/* reply(220,) must follow */
}
#ifndef VIRTUAL_HOSTING
if ((hostname = malloc(MAXHOSTNAMELEN)) == NULL)
fatalerror("Ran out of memory.");
if (gethostname(hostname, MAXHOSTNAMELEN - 1) < 0)
hostname[0] = '\0';
hostname[MAXHOSTNAMELEN - 1] = '\0';
#endif
if (hostinfo)
reply(220, "%s FTP server (%s) ready.", hostname, version);
else
reply(220, "FTP server ready.");
BLACKLIST_INIT();
for (;;)
(void) yyparse();
/* NOTREACHED */
}
static void
lostconn(int signo)
{
if (ftpdebug)
syslog(LOG_DEBUG, "lost connection");
dologout(1);
}
static void
sigquit(int signo)
{
syslog(LOG_ERR, "got signal %d", signo);
dologout(1);
}
#ifdef VIRTUAL_HOSTING
/*
* read in virtual host tables (if they exist)
*/
static void
inithosts(int family)
{
int insert;
size_t len;
FILE *fp;
char *cp, *mp, *line;
char *hostname;
char *vhost, *anonuser, *statfile, *welcome, *loginmsg;
struct ftphost *hrp, *lhrp;
struct addrinfo hints, *res, *ai;
/*
* Fill in the default host information
*/
if ((hostname = malloc(MAXHOSTNAMELEN)) == NULL)
fatalerror("Ran out of memory.");
if (gethostname(hostname, MAXHOSTNAMELEN - 1) < 0)
hostname[0] = '\0';
hostname[MAXHOSTNAMELEN - 1] = '\0';
if ((hrp = malloc(sizeof(struct ftphost))) == NULL)
fatalerror("Ran out of memory.");
hrp->hostname = hostname;
hrp->hostinfo = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_PASSIVE;
hints.ai_family = family;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(hrp->hostname, NULL, &hints, &res) == 0)
hrp->hostinfo = res;
hrp->statfile = _PATH_FTPDSTATFILE;
hrp->welcome = _PATH_FTPWELCOME;
hrp->loginmsg = _PATH_FTPLOGINMESG;
hrp->anonuser = "ftp";
hrp->next = NULL;
thishost = firsthost = lhrp = hrp;
if ((fp = fopen(_PATH_FTPHOSTS, "r")) != NULL) {
int addrsize, gothost;
void *addr;
struct hostent *hp;
while ((line = fgetln(fp, &len)) != NULL) {
int i, hp_error;
/* skip comments */
if (line[0] == '#')
continue;
if (line[len - 1] == '\n') {
line[len - 1] = '\0';
mp = NULL;
} else {
if ((mp = malloc(len + 1)) == NULL)
fatalerror("Ran out of memory.");
memcpy(mp, line, len);
mp[len] = '\0';
line = mp;
}
cp = strtok(line, " \t");
/* skip empty lines */
if (cp == NULL)
goto nextline;
vhost = cp;
/* set defaults */
anonuser = "ftp";
statfile = _PATH_FTPDSTATFILE;
welcome = _PATH_FTPWELCOME;
loginmsg = _PATH_FTPLOGINMESG;
/*
* Preparse the line so we can use its info
* for all the addresses associated with
* the virtual host name.
* Field 0, the virtual host name, is special:
* it's already parsed off and will be strdup'ed
* later, after we know its canonical form.
*/
for (i = 1; i < 5 && (cp = strtok(NULL, " \t")); i++)
if (*cp != '-' && (cp = strdup(cp)))
switch (i) {
case 1: /* anon user permissions */
anonuser = cp;
break;
case 2: /* statistics file */
statfile = cp;
break;
case 3: /* welcome message */
welcome = cp;
break;
case 4: /* login message */
loginmsg = cp;
break;
default: /* programming error */
abort();
/* NOTREACHED */
}
hints.ai_flags = AI_PASSIVE;
hints.ai_family = family;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(vhost, NULL, &hints, &res) != 0)
goto nextline;
for (ai = res; ai != NULL && ai->ai_addr != NULL;
ai = ai->ai_next) {
gothost = 0;
for (hrp = firsthost; hrp != NULL; hrp = hrp->next) {
struct addrinfo *hi;
for (hi = hrp->hostinfo; hi != NULL;
hi = hi->ai_next)
if (hi->ai_addrlen == ai->ai_addrlen &&
memcmp(hi->ai_addr,
ai->ai_addr,
ai->ai_addr->sa_len) == 0) {
gothost++;
break;
}
if (gothost)
break;
}
if (hrp == NULL) {
if ((hrp = malloc(sizeof(struct ftphost))) == NULL)
goto nextline;
hrp->hostname = NULL;
insert = 1;
} else {
if (hrp->hostinfo && hrp->hostinfo != res)
freeaddrinfo(hrp->hostinfo);
insert = 0; /* host already in the chain */
}
hrp->hostinfo = res;
/*
* determine hostname to use.
* force defined name if there is a valid alias
* otherwise fallback to primary hostname
*/
/* XXX: getaddrinfo() can't do alias check */
switch(hrp->hostinfo->ai_family) {
case AF_INET:
addr = &((struct sockaddr_in *)hrp->hostinfo->ai_addr)->sin_addr;
addrsize = sizeof(struct in_addr);
break;
case AF_INET6:
addr = &((struct sockaddr_in6 *)hrp->hostinfo->ai_addr)->sin6_addr;
addrsize = sizeof(struct in6_addr);
break;
default:
/* should not reach here */
freeaddrinfo(hrp->hostinfo);
if (insert)
free(hrp); /*not in chain, can free*/
else
hrp->hostinfo = NULL; /*mark as blank*/
goto nextline;
/* NOTREACHED */
}
if ((hp = getipnodebyaddr(addr, addrsize,
hrp->hostinfo->ai_family,
&hp_error)) != NULL) {
if (strcmp(vhost, hp->h_name) != 0) {
if (hp->h_aliases == NULL)
vhost = hp->h_name;
else {
i = 0;
while (hp->h_aliases[i] &&
strcmp(vhost, hp->h_aliases[i]) != 0)
++i;
if (hp->h_aliases[i] == NULL)
vhost = hp->h_name;
}
}
}
if (hrp->hostname &&
strcmp(hrp->hostname, vhost) != 0) {
free(hrp->hostname);
hrp->hostname = NULL;
}
if (hrp->hostname == NULL &&
(hrp->hostname = strdup(vhost)) == NULL) {
freeaddrinfo(hrp->hostinfo);
hrp->hostinfo = NULL; /* mark as blank */
if (hp)
freehostent(hp);
goto nextline;
}
hrp->anonuser = anonuser;
hrp->statfile = statfile;
hrp->welcome = welcome;
hrp->loginmsg = loginmsg;
if (insert) {
hrp->next = NULL;
lhrp->next = hrp;
lhrp = hrp;
}
if (hp)
freehostent(hp);
}
nextline:
if (mp)
free(mp);
}
(void) fclose(fp);
}
}
static void
selecthost(union sockunion *su)
{
struct ftphost *hrp;
u_int16_t port;
#ifdef INET6
struct in6_addr *mapped_in6 = NULL;
#endif
struct addrinfo *hi;
#ifdef INET6
/*
* XXX IPv4 mapped IPv6 addr consideraton,
* specified in rfc2373.
*/
if (su->su_family == AF_INET6 &&
IN6_IS_ADDR_V4MAPPED(&su->su_sin6.sin6_addr))
mapped_in6 = &su->su_sin6.sin6_addr;
#endif
hrp = thishost = firsthost; /* default */
port = su->su_port;
su->su_port = 0;
while (hrp != NULL) {
for (hi = hrp->hostinfo; hi != NULL; hi = hi->ai_next) {
if (memcmp(su, hi->ai_addr, hi->ai_addrlen) == 0) {
thishost = hrp;
goto found;
}
#ifdef INET6
/* XXX IPv4 mapped IPv6 addr consideraton */
if (hi->ai_addr->sa_family == AF_INET && mapped_in6 != NULL &&
(memcmp(&mapped_in6->s6_addr[12],
&((struct sockaddr_in *)hi->ai_addr)->sin_addr,
sizeof(struct in_addr)) == 0)) {
thishost = hrp;
goto found;
}
#endif
}
hrp = hrp->next;
}
found:
su->su_port = port;
/* setup static variables as appropriate */
hostname = thishost->hostname;
ftpuser = thishost->anonuser;
}
#endif
/*
* Helper function for sgetpwnam().
*/
static char *
sgetsave(char *s)
{
char *new = malloc(strlen(s) + 1);
if (new == NULL) {
reply(421, "Ran out of memory.");
dologout(1);
/* NOTREACHED */
}
(void) strcpy(new, s);
return (new);
}
/*
* Save the result of a getpwnam. Used for USER command, since
* the data returned must not be clobbered by any other command
* (e.g., globbing).
* NB: The data returned by sgetpwnam() will remain valid until
* the next call to this function. Its difference from getpwnam()
* is that sgetpwnam() is known to be called from ftpd code only.
*/
static struct passwd *
sgetpwnam(char *name)
{
static struct passwd save;
struct passwd *p;
if ((p = getpwnam(name)) == NULL)
return (p);
if (save.pw_name) {
free(save.pw_name);
free(save.pw_passwd);
free(save.pw_class);
free(save.pw_gecos);
free(save.pw_dir);
free(save.pw_shell);
}
save = *p;
save.pw_name = sgetsave(p->pw_name);
save.pw_passwd = sgetsave(p->pw_passwd);
save.pw_class = sgetsave(p->pw_class);
save.pw_gecos = sgetsave(p->pw_gecos);
save.pw_dir = sgetsave(p->pw_dir);
save.pw_shell = sgetsave(p->pw_shell);
return (&save);
}
static int login_attempts; /* number of failed login attempts */
static int askpasswd; /* had user command, ask for passwd */
static char curname[MAXLOGNAME]; /* current USER name */
/*
* USER command.
* Sets global passwd pointer pw if named account exists and is acceptable;
* sets askpasswd if a PASS command is expected. If logged in previously,
* need to reset state. If name is "ftp" or "anonymous", the name is not in
* _PATH_FTPUSERS, and ftp account exists, set guest and pw, then just return.
* If account doesn't exist, ask for passwd anyway. Otherwise, check user
* requesting login privileges. Disallow anyone who does not have a standard
* shell as returned by getusershell(). Disallow anyone mentioned in the file
* _PATH_FTPUSERS to allow people such as root and uucp to be avoided.
*/
void
user(char *name)
{
int ecode;
char *cp, *shell;
if (logged_in) {
if (guest) {
reply(530, "Can't change user from guest login.");
return;
} else if (dochroot) {
reply(530, "Can't change user from chroot user.");
return;
}
end_login();
}
guest = 0;
#ifdef VIRTUAL_HOSTING
pw = sgetpwnam(thishost->anonuser);
#else
pw = sgetpwnam("ftp");
#endif
if (strcmp(name, "ftp") == 0 || strcmp(name, "anonymous") == 0) {
if (checkuser(_PATH_FTPUSERS, "ftp", 0, NULL, &ecode) ||
(ecode != 0 && ecode != ENOENT))
reply(530, "User %s access denied.", name);
else if (checkuser(_PATH_FTPUSERS, "anonymous", 0, NULL, &ecode) ||
(ecode != 0 && ecode != ENOENT))
reply(530, "User %s access denied.", name);
else if (pw != NULL) {
guest = 1;
askpasswd = 1;
reply(331,
"Guest login ok, send your email address as password.");
} else
reply(530, "User %s unknown.", name);
if (!askpasswd && logging)
syslog(LOG_NOTICE,
"ANONYMOUS FTP LOGIN REFUSED FROM %s", remotehost);
return;
}
if (anon_only != 0) {
reply(530, "Sorry, only anonymous ftp allowed.");
return;
}
if ((pw = sgetpwnam(name))) {
if ((shell = pw->pw_shell) == NULL || *shell == 0)
shell = _PATH_BSHELL;
setusershell();
while ((cp = getusershell()) != NULL)
if (strcmp(cp, shell) == 0)
break;
endusershell();
if (cp == NULL ||
(checkuser(_PATH_FTPUSERS, name, 1, NULL, &ecode) ||
(ecode != 0 && ecode != ENOENT))) {
reply(530, "User %s access denied.", name);
if (logging)
syslog(LOG_NOTICE,
"FTP LOGIN REFUSED FROM %s, %s",
remotehost, name);
pw = NULL;
return;
}
}
if (logging)
strlcpy(curname, name, sizeof(curname));
reply(331, "Password required for %s.", name);
askpasswd = 1;
/*
* Delay before reading passwd after first failed
* attempt to slow down passwd-guessing programs.
*/
if (login_attempts)
sleep(login_attempts);
}
/*
* Check if a user is in the file "fname",
* return a pointer to a malloc'd string with the rest
* of the matching line in "residue" if not NULL.
*/
static int
checkuser(char *fname, char *name, int pwset, char **residue, int *ecode)
{
FILE *fd;
int found = 0;
size_t len;
char *line, *mp, *p;
if (ecode != NULL)
*ecode = 0;
if ((fd = fopen(fname, "r")) != NULL) {
while (!found && (line = fgetln(fd, &len)) != NULL) {
/* skip comments */
if (line[0] == '#')
continue;
if (line[len - 1] == '\n') {
line[len - 1] = '\0';
mp = NULL;
} else {
if ((mp = malloc(len + 1)) == NULL)
fatalerror("Ran out of memory.");
memcpy(mp, line, len);
mp[len] = '\0';
line = mp;
}
/* avoid possible leading and trailing whitespace */
p = strtok(line, " \t");
/* skip empty lines */
if (p == NULL)
goto nextline;
/*
* if first chr is '@', check group membership
*/
if (p[0] == '@') {
int i = 0;
struct group *grp;
if (p[1] == '\0') /* single @ matches anyone */
found = 1;
else {
if ((grp = getgrnam(p+1)) == NULL)
goto nextline;
/*
* Check user's default group
*/
if (pwset && grp->gr_gid == pw->pw_gid)
found = 1;
/*
* Check supplementary groups
*/
while (!found && grp->gr_mem[i])
found = strcmp(name,
grp->gr_mem[i++])
== 0;
}
}
/*
* Otherwise, just check for username match
*/
else
found = strcmp(p, name) == 0;
/*
* Save the rest of line to "residue" if matched
*/
if (found && residue) {
if ((p = strtok(NULL, "")) != NULL)
p += strspn(p, " \t");
if (p && *p) {
if ((*residue = strdup(p)) == NULL)
fatalerror("Ran out of memory.");
} else
*residue = NULL;
}
nextline:
if (mp)
free(mp);
}
(void) fclose(fd);
} else if (ecode != NULL)
*ecode = errno;
return (found);
}
/*
* Terminate login as previous user, if any, resetting state;
* used when USER command is given or login fails.
*/
static void
end_login(void)
{
#ifdef USE_PAM
int e;
#endif
(void) seteuid(0);
#ifdef LOGIN_CAP
setusercontext(NULL, getpwuid(0), 0, LOGIN_SETALL & ~(LOGIN_SETLOGIN |
LOGIN_SETUSER | LOGIN_SETGROUP | LOGIN_SETPATH |
LOGIN_SETENV));
#endif
if (logged_in && dowtmp)
ftpd_logwtmp(wtmpid, NULL, NULL);
pw = NULL;
#ifdef USE_PAM
if (pamh) {
if ((e = pam_setcred(pamh, PAM_DELETE_CRED)) != PAM_SUCCESS)
syslog(LOG_ERR, "pam_setcred: %s", pam_strerror(pamh, e));
if ((e = pam_close_session(pamh,0)) != PAM_SUCCESS)
syslog(LOG_ERR, "pam_close_session: %s", pam_strerror(pamh, e));
if ((e = pam_end(pamh, e)) != PAM_SUCCESS)
syslog(LOG_ERR, "pam_end: %s", pam_strerror(pamh, e));
pamh = NULL;
}
#endif
logged_in = 0;
guest = 0;
dochroot = 0;
}
#ifdef USE_PAM
/*
* the following code is stolen from imap-uw PAM authentication module and
* login.c
*/
#define COPY_STRING(s) (s ? strdup(s) : NULL)
struct cred_t {
const char *uname; /* user name */
const char *pass; /* password */
};
typedef struct cred_t cred_t;
static int
auth_conv(int num_msg, const struct pam_message **msg,
struct pam_response **resp, void *appdata)
{
int i;
cred_t *cred = (cred_t *) appdata;
struct pam_response *reply;
reply = calloc(num_msg, sizeof *reply);
if (reply == NULL)
return PAM_BUF_ERR;
for (i = 0; i < num_msg; i++) {
switch (msg[i]->msg_style) {
case PAM_PROMPT_ECHO_ON: /* assume want user name */
reply[i].resp_retcode = PAM_SUCCESS;
reply[i].resp = COPY_STRING(cred->uname);
/* PAM frees resp. */
break;
case PAM_PROMPT_ECHO_OFF: /* assume want password */
reply[i].resp_retcode = PAM_SUCCESS;
reply[i].resp = COPY_STRING(cred->pass);
/* PAM frees resp. */
break;
case PAM_TEXT_INFO:
case PAM_ERROR_MSG:
reply[i].resp_retcode = PAM_SUCCESS;
reply[i].resp = NULL;
break;
default: /* unknown message style */
free(reply);
return PAM_CONV_ERR;
}
}
*resp = reply;
return PAM_SUCCESS;
}
/*
* Attempt to authenticate the user using PAM. Returns 0 if the user is
* authenticated, or 1 if not authenticated. If some sort of PAM system
* error occurs (e.g., the "/etc/pam.conf" file is missing) then this
* function returns -1. This can be used as an indication that we should
* fall back to a different authentication mechanism.
*/
static int
auth_pam(struct passwd **ppw, const char *pass)
{
const char *tmpl_user;
const void *item;
int rval;
int e;
cred_t auth_cred = { (*ppw)->pw_name, pass };
struct pam_conv conv = { &auth_conv, &auth_cred };
e = pam_start("ftpd", (*ppw)->pw_name, &conv, &pamh);
if (e != PAM_SUCCESS) {
/*
* In OpenPAM, it's OK to pass NULL to pam_strerror()
* if context creation has failed in the first place.
*/
syslog(LOG_ERR, "pam_start: %s", pam_strerror(NULL, e));
return -1;
}
e = pam_set_item(pamh, PAM_RHOST, remotehost);
if (e != PAM_SUCCESS) {
syslog(LOG_ERR, "pam_set_item(PAM_RHOST): %s",
pam_strerror(pamh, e));
if ((e = pam_end(pamh, e)) != PAM_SUCCESS) {
syslog(LOG_ERR, "pam_end: %s", pam_strerror(pamh, e));
}
pamh = NULL;
return -1;
}
e = pam_authenticate(pamh, 0);
switch (e) {
case PAM_SUCCESS:
/*
* With PAM we support the concept of a "template"
* user. The user enters a login name which is
* authenticated by PAM, usually via a remote service
* such as RADIUS or TACACS+. If authentication
* succeeds, a different but related "template" name
* is used for setting the credentials, shell, and
* home directory. The name the user enters need only
* exist on the remote authentication server, but the
* template name must be present in the local password
* database.
*
* This is supported by two various mechanisms in the
* individual modules. However, from the application's
* point of view, the template user is always passed
* back as a changed value of the PAM_USER item.
*/
if ((e = pam_get_item(pamh, PAM_USER, &item)) ==
PAM_SUCCESS) {
tmpl_user = (const char *) item;
if (strcmp((*ppw)->pw_name, tmpl_user) != 0)
*ppw = getpwnam(tmpl_user);
} else
syslog(LOG_ERR, "Couldn't get PAM_USER: %s",
pam_strerror(pamh, e));
rval = 0;
break;
case PAM_AUTH_ERR:
case PAM_USER_UNKNOWN:
case PAM_MAXTRIES:
rval = 1;
break;
default:
syslog(LOG_ERR, "pam_authenticate: %s", pam_strerror(pamh, e));
rval = -1;
break;
}
if (rval == 0) {
e = pam_acct_mgmt(pamh, 0);
if (e != PAM_SUCCESS) {
syslog(LOG_ERR, "pam_acct_mgmt: %s",
pam_strerror(pamh, e));
rval = 1;
}
}
if (rval != 0) {
if ((e = pam_end(pamh, e)) != PAM_SUCCESS) {
syslog(LOG_ERR, "pam_end: %s", pam_strerror(pamh, e));
}
pamh = NULL;
}
return rval;
}
#endif /* USE_PAM */
void
pass(char *passwd)
{
int rval, ecode;
FILE *fd;
#ifdef LOGIN_CAP
login_cap_t *lc = NULL;
#endif
#ifdef USE_PAM
int e;
#endif
char *residue = NULL;
char *xpasswd;
if (logged_in || askpasswd == 0) {
reply(503, "Login with USER first.");
return;
}
askpasswd = 0;
if (!guest) { /* "ftp" is only account allowed no password */
if (pw == NULL) {
rval = 1; /* failure below */
goto skip;
}
#ifdef USE_PAM
rval = auth_pam(&pw, passwd);
if (rval >= 0) {
goto skip;
}
#endif
xpasswd = crypt(passwd, pw->pw_passwd);
if (passwd[0] == '\0' && pw->pw_passwd[0] != '\0')
xpasswd = ":";
rval = strcmp(pw->pw_passwd, xpasswd);
if (pw->pw_expire && time(NULL) >= pw->pw_expire)
rval = 1; /* failure */
skip:
/*
* If rval == 1, the user failed the authentication check
* above. If rval == 0, either PAM or local authentication
* succeeded.
*/
if (rval) {
reply(530, "Login incorrect.");
BLACKLIST_NOTIFY(BLACKLIST_AUTH_FAIL, STDIN_FILENO, "Login incorrect");
if (logging) {
syslog(LOG_NOTICE,
"FTP LOGIN FAILED FROM %s",
remotehost);
syslog(LOG_AUTHPRIV | LOG_NOTICE,
"FTP LOGIN FAILED FROM %s, %s",
remotehost, curname);
}
pw = NULL;
if (login_attempts++ >= 5) {
syslog(LOG_NOTICE,
"repeated login failures from %s",
remotehost);
exit(0);
}
return;
} else {
BLACKLIST_NOTIFY(BLACKLIST_AUTH_OK, STDIN_FILENO, "Login successful");
}
}
login_attempts = 0; /* this time successful */
if (setegid(pw->pw_gid) < 0) {
reply(550, "Can't set gid.");
return;
}
/* May be overridden by login.conf */
(void) umask(defumask);
#ifdef LOGIN_CAP
if ((lc = login_getpwclass(pw)) != NULL) {
char remote_ip[NI_MAXHOST];
if (getnameinfo((struct sockaddr *)&his_addr, his_addr.su_len,
remote_ip, sizeof(remote_ip) - 1, NULL, 0,
NI_NUMERICHOST))
*remote_ip = 0;
remote_ip[sizeof(remote_ip) - 1] = 0;
if (!auth_hostok(lc, remotehost, remote_ip)) {
syslog(LOG_INFO|LOG_AUTH,
"FTP LOGIN FAILED (HOST) as %s: permission denied.",
pw->pw_name);
reply(530, "Permission denied.");
pw = NULL;
return;
}
if (!auth_timeok(lc, time(NULL))) {
reply(530, "Login not available right now.");
pw = NULL;
return;
}
}
setusercontext(lc, pw, 0, LOGIN_SETALL &
~(LOGIN_SETRESOURCES | LOGIN_SETUSER | LOGIN_SETPATH | LOGIN_SETENV));
#else
setlogin(pw->pw_name);
(void) initgroups(pw->pw_name, pw->pw_gid);
#endif
#ifdef USE_PAM
if (pamh) {
if ((e = pam_open_session(pamh, 0)) != PAM_SUCCESS) {
syslog(LOG_ERR, "pam_open_session: %s", pam_strerror(pamh, e));
} else if ((e = pam_setcred(pamh, PAM_ESTABLISH_CRED)) != PAM_SUCCESS) {
syslog(LOG_ERR, "pam_setcred: %s", pam_strerror(pamh, e));
}
}
#endif
dochroot =
checkuser(_PATH_FTPCHROOT, pw->pw_name, 1, &residue, &ecode)
#ifdef LOGIN_CAP /* Allow login.conf configuration as well */
|| login_getcapbool(lc, "ftp-chroot", 0)
#endif
;
/*
* It is possible that checkuser() failed to open the chroot file.
* If this is the case, report that logins are un-available, since we
* have no way of checking whether or not the user should be chrooted.
* We ignore ENOENT since it is not required that this file be present.
*/
if (ecode != 0 && ecode != ENOENT) {
reply(530, "Login not available right now.");
return;
}
chrootdir = NULL;
/* Disable wtmp logging when chrooting. */
if (dochroot || guest)
dowtmp = 0;
if (dowtmp)
ftpd_logwtmp(wtmpid, pw->pw_name,
(struct sockaddr *)&his_addr);
logged_in = 1;
#ifdef LOGIN_CAP
setusercontext(lc, pw, 0, LOGIN_SETRESOURCES);
#endif
if (guest && stats && statfd < 0) {
#ifdef VIRTUAL_HOSTING
statfd = open(thishost->statfile, O_WRONLY|O_APPEND);
#else
statfd = open(_PATH_FTPDSTATFILE, O_WRONLY|O_APPEND);
#endif
if (statfd < 0)
stats = 0;
}
/*
* For a chrooted local user,
* a) see whether ftpchroot(5) specifies a chroot directory,
* b) extract the directory pathname from the line,
* c) expand it to the absolute pathname if necessary.
*/
if (dochroot && residue &&
(chrootdir = strtok(residue, " \t")) != NULL) {
if (chrootdir[0] != '/')
asprintf(&chrootdir, "%s/%s", pw->pw_dir, chrootdir);
else
chrootdir = strdup(chrootdir); /* make it permanent */
if (chrootdir == NULL)
fatalerror("Ran out of memory.");
}
if (guest || dochroot) {
/*
* If no chroot directory set yet, use the login directory.
* Copy it so it can be modified while pw->pw_dir stays intact.
*/
if (chrootdir == NULL &&
(chrootdir = strdup(pw->pw_dir)) == NULL)
fatalerror("Ran out of memory.");
/*
* Check for the "/chroot/./home" syntax,
* separate the chroot and home directory pathnames.
*/
if ((homedir = strstr(chrootdir, "/./")) != NULL) {
*(homedir++) = '\0'; /* wipe '/' */
homedir++; /* skip '.' */
} else {
/*
* We MUST do a chdir() after the chroot. Otherwise
* the old current directory will be accessible as "."
* outside the new root!
*/
homedir = "/";
}
/*
* Finally, do chroot()
*/
if (chroot(chrootdir) < 0) {
reply(550, "Can't change root.");
goto bad;
}
__FreeBSD_libc_enter_restricted_mode();
} else /* real user w/o chroot */
homedir = pw->pw_dir;
/*
* Set euid *before* doing chdir() so
* a) the user won't be carried to a directory that he couldn't reach
* on his own due to no permission to upper path components,
* b) NFS mounted homedirs w/restrictive permissions will be accessible
* (uid 0 has no root power over NFS if not mapped explicitly.)
*/
if (seteuid(pw->pw_uid) < 0) {
if (guest || dochroot) {
fatalerror("Can't set uid.");
} else {
reply(550, "Can't set uid.");
goto bad;
}
}
/*
* Do not allow the session to live if we're chroot()'ed and chdir()
* fails. Otherwise the chroot jail can be escaped.
*/
if (chdir(homedir) < 0) {
if (guest || dochroot) {
fatalerror("Can't change to base directory.");
} else {
if (chdir("/") < 0) {
reply(550, "Root is inaccessible.");
goto bad;
}
lreply(230, "No directory! Logging in with home=/.");
}
}
/*
* Display a login message, if it exists.
* N.B. reply(230,) must follow the message.
*/
#ifdef VIRTUAL_HOSTING
fd = fopen(thishost->loginmsg, "r");
#else
fd = fopen(_PATH_FTPLOGINMESG, "r");
#endif
if (fd != NULL) {
char *cp, line[LINE_MAX];
while (fgets(line, sizeof(line), fd) != NULL) {
if ((cp = strchr(line, '\n')) != NULL)
*cp = '\0';
lreply(230, "%s", line);
}
(void) fflush(stdout);
(void) fclose(fd);
}
if (guest) {
if (ident != NULL)
free(ident);
ident = strdup(passwd);
if (ident == NULL)
fatalerror("Ran out of memory.");
reply(230, "Guest login ok, access restrictions apply.");
#ifdef SETPROCTITLE
#ifdef VIRTUAL_HOSTING
if (thishost != firsthost)
snprintf(proctitle, sizeof(proctitle),
"%s: anonymous(%s)/%s", remotehost, hostname,
passwd);
else
#endif
snprintf(proctitle, sizeof(proctitle),
"%s: anonymous/%s", remotehost, passwd);
setproctitle("%s", proctitle);
#endif /* SETPROCTITLE */
if (logging)
syslog(LOG_INFO, "ANONYMOUS FTP LOGIN FROM %s, %s",
remotehost, passwd);
} else {
if (dochroot)
reply(230, "User %s logged in, "
"access restrictions apply.", pw->pw_name);
else
reply(230, "User %s logged in.", pw->pw_name);
#ifdef SETPROCTITLE
snprintf(proctitle, sizeof(proctitle),
"%s: user/%s", remotehost, pw->pw_name);
setproctitle("%s", proctitle);
#endif /* SETPROCTITLE */
if (logging)
syslog(LOG_INFO, "FTP LOGIN FROM %s as %s",
remotehost, pw->pw_name);
}
if (logging && (guest || dochroot))
syslog(LOG_INFO, "session root changed to %s", chrootdir);
#ifdef LOGIN_CAP
login_close(lc);
#endif
if (residue)
free(residue);
return;
bad:
/* Forget all about it... */
#ifdef LOGIN_CAP
login_close(lc);
#endif
if (residue)
free(residue);
end_login();
}
void
retrieve(char *cmd, char *name)
{
FILE *fin, *dout;
struct stat st;
int (*closefunc)(FILE *);
time_t start;
char line[BUFSIZ];
if (cmd == 0) {
fin = fopen(name, "r"), closefunc = fclose;
st.st_size = 0;
} else {
(void) snprintf(line, sizeof(line), cmd, name);
name = line;
fin = ftpd_popen(line, "r"), closefunc = ftpd_pclose;
st.st_size = -1;
st.st_blksize = BUFSIZ;
}
if (fin == NULL) {
if (errno != 0) {
perror_reply(550, name);
if (cmd == 0) {
LOGCMD("get", name);
}
}
return;
}
byte_count = -1;
if (cmd == 0) {
if (fstat(fileno(fin), &st) < 0) {
perror_reply(550, name);
goto done;
}
if (!S_ISREG(st.st_mode)) {
/*
* Never sending a raw directory is a workaround
* for buggy clients that will attempt to RETR
* a directory before listing it, e.g., Mozilla.
* Preventing a guest from getting irregular files
* is a simple security measure.
*/
if (S_ISDIR(st.st_mode) || guest) {
reply(550, "%s: not a plain file.", name);
goto done;
}
st.st_size = -1;
/* st.st_blksize is set for all descriptor types */
}
}
if (restart_point) {
if (type == TYPE_A) {
off_t i, n;
int c;
n = restart_point;
i = 0;
while (i++ < n) {
if ((c=getc(fin)) == EOF) {
perror_reply(550, name);
goto done;
}
if (c == '\n')
i++;
}
} else if (lseek(fileno(fin), restart_point, L_SET) < 0) {
perror_reply(550, name);
goto done;
}
}
dout = dataconn(name, st.st_size, "w");
if (dout == NULL)
goto done;
time(&start);
send_data(fin, dout, st.st_blksize, st.st_size,
restart_point == 0 && cmd == 0 && S_ISREG(st.st_mode));
if (cmd == 0 && guest && stats && byte_count > 0)
logxfer(name, byte_count, start);
(void) fclose(dout);
data = -1;
pdata = -1;
done:
if (cmd == 0)
LOGBYTES("get", name, byte_count);
(*closefunc)(fin);
}
void
store(char *name, char *mode, int unique)
{
int fd;
FILE *fout, *din;
int (*closefunc)(FILE *);
if (*mode == 'a') { /* APPE */
if (unique) {
/* Programming error */
syslog(LOG_ERR, "Internal: unique flag to APPE");
unique = 0;
}
if (guest && noguestmod) {
reply(550, "Appending to existing file denied.");
goto err;
}
restart_point = 0; /* not affected by preceding REST */
}
if (unique) /* STOU overrides REST */
restart_point = 0;
if (guest && noguestmod) {
if (restart_point) { /* guest STOR w/REST */
reply(550, "Modifying existing file denied.");
goto err;
} else /* treat guest STOR as STOU */
unique = 1;
}
if (restart_point)
mode = "r+"; /* so ASCII manual seek can work */
if (unique) {
if ((fd = guniquefd(name, &name)) < 0)
goto err;
fout = fdopen(fd, mode);
} else
fout = fopen(name, mode);
closefunc = fclose;
if (fout == NULL) {
perror_reply(553, name);
goto err;
}
byte_count = -1;
if (restart_point) {
if (type == TYPE_A) {
off_t i, n;
int c;
n = restart_point;
i = 0;
while (i++ < n) {
if ((c=getc(fout)) == EOF) {
perror_reply(550, name);
goto done;
}
if (c == '\n')
i++;
}
/*
* We must do this seek to "current" position
* because we are changing from reading to
* writing.
*/
if (fseeko(fout, 0, SEEK_CUR) < 0) {
perror_reply(550, name);
goto done;
}
} else if (lseek(fileno(fout), restart_point, L_SET) < 0) {
perror_reply(550, name);
goto done;
}
}
din = dataconn(name, -1, "r");
if (din == NULL)
goto done;
if (receive_data(din, fout) == 0) {
if (unique)
reply(226, "Transfer complete (unique file name:%s).",
name);
else
reply(226, "Transfer complete.");
}
(void) fclose(din);
data = -1;
pdata = -1;
done:
LOGBYTES(*mode == 'a' ? "append" : "put", name, byte_count);
(*closefunc)(fout);
return;
err:
LOGCMD(*mode == 'a' ? "append" : "put" , name);
return;
}
static FILE *
getdatasock(char *mode)
{
int on = 1, s, t, tries;
if (data >= 0)
return (fdopen(data, mode));
s = socket(data_dest.su_family, SOCK_STREAM, 0);
if (s < 0)
goto bad;
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
syslog(LOG_WARNING, "data setsockopt (SO_REUSEADDR): %m");
/* anchor socket to avoid multi-homing problems */
data_source = ctrl_addr;
data_source.su_port = htons(dataport);
(void) seteuid(0);
for (tries = 1; ; tries++) {
/*
* We should loop here since it's possible that
* another ftpd instance has passed this point and is
* trying to open a data connection in active mode now.
* Until the other connection is opened, we'll be getting
* EADDRINUSE because no SOCK_STREAM sockets in the system
* can share both local and remote addresses, localIP:20
* and *:* in this case.
*/
if (bind(s, (struct sockaddr *)&data_source,
data_source.su_len) >= 0)
break;
if (errno != EADDRINUSE || tries > 10)
goto bad;
sleep(tries);
}
(void) seteuid(pw->pw_uid);
#ifdef IP_TOS
if (data_source.su_family == AF_INET)
{
on = IPTOS_THROUGHPUT;
if (setsockopt(s, IPPROTO_IP, IP_TOS, &on, sizeof(int)) < 0)
syslog(LOG_WARNING, "data setsockopt (IP_TOS): %m");
}
#endif
#ifdef TCP_NOPUSH
/*
* Turn off push flag to keep sender TCP from sending short packets
* at the boundaries of each write().
*/
on = 1;
if (setsockopt(s, IPPROTO_TCP, TCP_NOPUSH, &on, sizeof on) < 0)
syslog(LOG_WARNING, "data setsockopt (TCP_NOPUSH): %m");
#endif
return (fdopen(s, mode));
bad:
/* Return the real value of errno (close may change it) */
t = errno;
(void) seteuid(pw->pw_uid);
(void) close(s);
errno = t;
return (NULL);
}
static FILE *
dataconn(char *name, off_t size, char *mode)
{
char sizebuf[32];
FILE *file;
int retry = 0, tos, conerrno;
file_size = size;
byte_count = 0;
if (size != -1)
(void) snprintf(sizebuf, sizeof(sizebuf),
" (%jd bytes)", (intmax_t)size);
else
*sizebuf = '\0';
if (pdata >= 0) {
union sockunion from;
socklen_t fromlen = ctrl_addr.su_len;
int flags, s;
struct timeval timeout;
fd_set set;
FD_ZERO(&set);
FD_SET(pdata, &set);
timeout.tv_usec = 0;
timeout.tv_sec = 120;
/*
* Granted a socket is in the blocking I/O mode,
* accept() will block after a successful select()
* if the selected connection dies in between.
* Therefore set the non-blocking I/O flag here.
*/
if ((flags = fcntl(pdata, F_GETFL, 0)) == -1 ||
fcntl(pdata, F_SETFL, flags | O_NONBLOCK) == -1)
goto pdata_err;
if (select(pdata+1, &set, NULL, NULL, &timeout) <= 0 ||
(s = accept(pdata, (struct sockaddr *) &from, &fromlen)) < 0)
goto pdata_err;
(void) close(pdata);
pdata = s;
/*
* Unset the inherited non-blocking I/O flag
* on the child socket so stdio can work on it.
*/
if ((flags = fcntl(pdata, F_GETFL, 0)) == -1 ||
fcntl(pdata, F_SETFL, flags & ~O_NONBLOCK) == -1)
goto pdata_err;
#ifdef IP_TOS
if (from.su_family == AF_INET)
{
tos = IPTOS_THROUGHPUT;
if (setsockopt(s, IPPROTO_IP, IP_TOS, &tos, sizeof(int)) < 0)
syslog(LOG_WARNING, "pdata setsockopt (IP_TOS): %m");
}
#endif
reply(150, "Opening %s mode data connection for '%s'%s.",
type == TYPE_A ? "ASCII" : "BINARY", name, sizebuf);
return (fdopen(pdata, mode));
pdata_err:
reply(425, "Can't open data connection.");
(void) close(pdata);
pdata = -1;
return (NULL);
}
if (data >= 0) {
reply(125, "Using existing data connection for '%s'%s.",
name, sizebuf);
usedefault = 1;
return (fdopen(data, mode));
}
if (usedefault)
data_dest = his_addr;
usedefault = 1;
do {
file = getdatasock(mode);
if (file == NULL) {
char hostbuf[NI_MAXHOST], portbuf[NI_MAXSERV];
if (getnameinfo((struct sockaddr *)&data_source,
data_source.su_len,
hostbuf, sizeof(hostbuf) - 1,
portbuf, sizeof(portbuf) - 1,
NI_NUMERICHOST|NI_NUMERICSERV))
*hostbuf = *portbuf = 0;
hostbuf[sizeof(hostbuf) - 1] = 0;
portbuf[sizeof(portbuf) - 1] = 0;
reply(425, "Can't create data socket (%s,%s): %s.",
hostbuf, portbuf, strerror(errno));
return (NULL);
}
data = fileno(file);
conerrno = 0;
if (connect(data, (struct sockaddr *)&data_dest,
data_dest.su_len) == 0)
break;
conerrno = errno;
(void) fclose(file);
data = -1;
if (conerrno == EADDRINUSE) {
sleep(swaitint);
retry += swaitint;
} else {
break;
}
} while (retry <= swaitmax);
if (conerrno != 0) {
reply(425, "Can't build data connection: %s.",
strerror(conerrno));
return (NULL);
}
reply(150, "Opening %s mode data connection for '%s'%s.",
type == TYPE_A ? "ASCII" : "BINARY", name, sizebuf);
return (file);
}
/*
* A helper macro to avoid code duplication
* in send_data() and receive_data().
*
* XXX We have to block SIGURG during putc() because BSD stdio
* is unable to restart interrupted write operations and hence
* the entire buffer contents will be lost as soon as a write()
* call indicates EINTR to stdio.
*/
#define FTPD_PUTC(ch, file, label) \
do { \
int ret; \
\
do { \
START_UNSAFE; \
ret = putc((ch), (file)); \
END_UNSAFE; \
CHECKOOB(return (-1)) \
else if (ferror(file)) \
goto label; \
clearerr(file); \
} while (ret == EOF); \
} while (0)
/*
* Transfer the contents of "instr" to "outstr" peer using the appropriate
* encapsulation of the data subject to Mode, Structure, and Type.
*
* NB: Form isn't handled.
*/
static int
send_data(FILE *instr, FILE *outstr, size_t blksize, off_t filesize, int isreg)
{
int c, cp, filefd, netfd;
char *buf;
STARTXFER;
switch (type) {
case TYPE_A:
cp = EOF;
for (;;) {
c = getc(instr);
CHECKOOB(return (-1))
else if (c == EOF && ferror(instr))
goto file_err;
if (c == EOF) {
if (ferror(instr)) { /* resume after OOB */
clearerr(instr);
continue;
}
if (feof(instr)) /* EOF */
break;
syslog(LOG_ERR, "Internal: impossible condition"
" on file after getc()");
goto file_err;
}
if (c == '\n' && cp != '\r') {
FTPD_PUTC('\r', outstr, data_err);
byte_count++;
}
FTPD_PUTC(c, outstr, data_err);
byte_count++;
cp = c;
}
#ifdef notyet /* BSD stdio isn't ready for that */
while (fflush(outstr) == EOF) {
CHECKOOB(return (-1))
else
goto data_err;
clearerr(outstr);
}
ENDXFER;
#else
ENDXFER;
if (fflush(outstr) == EOF)
goto data_err;
#endif
reply(226, "Transfer complete.");
return (0);
case TYPE_I:
case TYPE_L:
/*
* isreg is only set if we are not doing restart and we
* are sending a regular file
*/
netfd = fileno(outstr);
filefd = fileno(instr);
if (isreg) {
char *msg = "Transfer complete.";
off_t cnt, offset;
int err;
cnt = offset = 0;
while (filesize > 0) {
err = sendfile(filefd, netfd, offset, 0,
NULL, &cnt, 0);
/*
* Calculate byte_count before OOB processing.
* It can be used in myoob() later.
*/
byte_count += cnt;
offset += cnt;
filesize -= cnt;
CHECKOOB(return (-1))
else if (err == -1) {
if (errno != EINTR &&
cnt == 0 && offset == 0)
goto oldway;
goto data_err;
}
if (err == -1) /* resume after OOB */
continue;
/*
* We hit the EOF prematurely.
* Perhaps the file was externally truncated.
*/
if (cnt == 0) {
msg = "Transfer finished due to "
"premature end of file.";
break;
}
}
ENDXFER;
reply(226, "%s", msg);
return (0);
}
oldway:
if ((buf = malloc(blksize)) == NULL) {
ENDXFER;
reply(451, "Ran out of memory.");
return (-1);
}
for (;;) {
int cnt, len;
char *bp;
cnt = read(filefd, buf, blksize);
CHECKOOB(free(buf); return (-1))
else if (cnt < 0) {
free(buf);
goto file_err;
}
if (cnt < 0) /* resume after OOB */
continue;
if (cnt == 0) /* EOF */
break;
for (len = cnt, bp = buf; len > 0;) {
cnt = write(netfd, bp, len);
CHECKOOB(free(buf); return (-1))
else if (cnt < 0) {
free(buf);
goto data_err;
}
if (cnt <= 0)
continue;
len -= cnt;
bp += cnt;
byte_count += cnt;
}
}
ENDXFER;
free(buf);
reply(226, "Transfer complete.");
return (0);
default:
ENDXFER;
reply(550, "Unimplemented TYPE %d in send_data.", type);
return (-1);
}
data_err:
ENDXFER;
perror_reply(426, "Data connection");
return (-1);
file_err:
ENDXFER;
perror_reply(551, "Error on input file");
return (-1);
}
/*
* Transfer data from peer to "outstr" using the appropriate encapulation of
* the data subject to Mode, Structure, and Type.
*
* N.B.: Form isn't handled.
*/
static int
receive_data(FILE *instr, FILE *outstr)
{
int c, cp;
int bare_lfs = 0;
STARTXFER;
switch (type) {
case TYPE_I:
case TYPE_L:
for (;;) {
int cnt, len;
char *bp;
char buf[BUFSIZ];
cnt = read(fileno(instr), buf, sizeof(buf));
CHECKOOB(return (-1))
else if (cnt < 0)
goto data_err;
if (cnt < 0) /* resume after OOB */
continue;
if (cnt == 0) /* EOF */
break;
for (len = cnt, bp = buf; len > 0;) {
cnt = write(fileno(outstr), bp, len);
CHECKOOB(return (-1))
else if (cnt < 0)
goto file_err;
if (cnt <= 0)
continue;
len -= cnt;
bp += cnt;
byte_count += cnt;
}
}
ENDXFER;
return (0);
case TYPE_E:
ENDXFER;
reply(553, "TYPE E not implemented.");
return (-1);
case TYPE_A:
cp = EOF;
for (;;) {
c = getc(instr);
CHECKOOB(return (-1))
else if (c == EOF && ferror(instr))
goto data_err;
if (c == EOF && ferror(instr)) { /* resume after OOB */
clearerr(instr);
continue;
}
if (cp == '\r') {
if (c != '\n')
FTPD_PUTC('\r', outstr, file_err);
} else
if (c == '\n')
bare_lfs++;
if (c == '\r') {
byte_count++;
cp = c;
continue;
}
/* Check for EOF here in order not to lose last \r. */
if (c == EOF) {
if (feof(instr)) /* EOF */
break;
syslog(LOG_ERR, "Internal: impossible condition"
" on data stream after getc()");
goto data_err;
}
byte_count++;
FTPD_PUTC(c, outstr, file_err);
cp = c;
}
#ifdef notyet /* BSD stdio isn't ready for that */
while (fflush(outstr) == EOF) {
CHECKOOB(return (-1))
else
goto file_err;
clearerr(outstr);
}
ENDXFER;
#else
ENDXFER;
if (fflush(outstr) == EOF)
goto file_err;
#endif
if (bare_lfs) {
lreply(226,
"WARNING! %d bare linefeeds received in ASCII mode.",
bare_lfs);
(void)printf(" File may not have transferred correctly.\r\n");
}
return (0);
default:
ENDXFER;
reply(550, "Unimplemented TYPE %d in receive_data.", type);
return (-1);
}
data_err:
ENDXFER;
perror_reply(426, "Data connection");
return (-1);
file_err:
ENDXFER;
perror_reply(452, "Error writing to file");
return (-1);
}
void
statfilecmd(char *filename)
{
FILE *fin;
int atstart;
int c, code;
char line[LINE_MAX];
struct stat st;
code = lstat(filename, &st) == 0 && S_ISDIR(st.st_mode) ? 212 : 213;
(void)snprintf(line, sizeof(line), _PATH_LS " -lA %s", filename);
fin = ftpd_popen(line, "r");
if (fin == NULL) {
perror_reply(551, filename);
return;
}
lreply(code, "Status of %s:", filename);
atstart = 1;
while ((c = getc(fin)) != EOF) {
if (c == '\n') {
if (ferror(stdout)){
perror_reply(421, "Control connection");
(void) ftpd_pclose(fin);
dologout(1);
/* NOTREACHED */
}
if (ferror(fin)) {
perror_reply(551, filename);
(void) ftpd_pclose(fin);
return;
}
(void) putc('\r', stdout);
}
/*
* RFC 959 says neutral text should be prepended before
* a leading 3-digit number followed by whitespace, but
* many ftp clients can be confused by any leading digits,
* as a matter of fact.
*/
if (atstart && isdigit(c))
(void) putc(' ', stdout);
(void) putc(c, stdout);
atstart = (c == '\n');
}
(void) ftpd_pclose(fin);
reply(code, "End of status.");
}
void
statcmd(void)
{
union sockunion *su;
u_char *a, *p;
char hname[NI_MAXHOST];
int ispassive;
if (hostinfo) {
lreply(211, "%s FTP server status:", hostname);
printf(" %s\r\n", version);
} else
lreply(211, "FTP server status:");
printf(" Connected to %s", remotehost);
if (!getnameinfo((struct sockaddr *)&his_addr, his_addr.su_len,
hname, sizeof(hname) - 1, NULL, 0, NI_NUMERICHOST)) {
hname[sizeof(hname) - 1] = 0;
if (strcmp(hname, remotehost) != 0)
printf(" (%s)", hname);
}
printf("\r\n");
if (logged_in) {
if (guest)
printf(" Logged in anonymously\r\n");
else
printf(" Logged in as %s\r\n", pw->pw_name);
} else if (askpasswd)
printf(" Waiting for password\r\n");
else
printf(" Waiting for user name\r\n");
printf(" TYPE: %s", typenames[type]);
if (type == TYPE_A || type == TYPE_E)
printf(", FORM: %s", formnames[form]);
if (type == TYPE_L)
#if CHAR_BIT == 8
printf(" %d", CHAR_BIT);
#else
printf(" %d", bytesize); /* need definition! */
#endif
printf("; STRUcture: %s; transfer MODE: %s\r\n",
strunames[stru], modenames[mode]);
if (data != -1)
printf(" Data connection open\r\n");
else if (pdata != -1) {
ispassive = 1;
su = &pasv_addr;
goto printaddr;
} else if (usedefault == 0) {
ispassive = 0;
su = &data_dest;
printaddr:
#define UC(b) (((int) b) & 0xff)
if (epsvall) {
printf(" EPSV only mode (EPSV ALL)\r\n");
goto epsvonly;
}
/* PORT/PASV */
if (su->su_family == AF_INET) {
a = (u_char *) &su->su_sin.sin_addr;
p = (u_char *) &su->su_sin.sin_port;
printf(" %s (%d,%d,%d,%d,%d,%d)\r\n",
ispassive ? "PASV" : "PORT",
UC(a[0]), UC(a[1]), UC(a[2]), UC(a[3]),
UC(p[0]), UC(p[1]));
}
/* LPRT/LPSV */
{
int alen, af, i;
switch (su->su_family) {
case AF_INET:
a = (u_char *) &su->su_sin.sin_addr;
p = (u_char *) &su->su_sin.sin_port;
alen = sizeof(su->su_sin.sin_addr);
af = 4;
break;
case AF_INET6:
a = (u_char *) &su->su_sin6.sin6_addr;
p = (u_char *) &su->su_sin6.sin6_port;
alen = sizeof(su->su_sin6.sin6_addr);
af = 6;
break;
default:
af = 0;
break;
}
if (af) {
printf(" %s (%d,%d,", ispassive ? "LPSV" : "LPRT",
af, alen);
for (i = 0; i < alen; i++)
printf("%d,", UC(a[i]));
printf("%d,%d,%d)\r\n", 2, UC(p[0]), UC(p[1]));
}
}
epsvonly:;
/* EPRT/EPSV */
{
int af;
switch (su->su_family) {
case AF_INET:
af = 1;
break;
case AF_INET6:
af = 2;
break;
default:
af = 0;
break;
}
if (af) {
union sockunion tmp;
tmp = *su;
if (tmp.su_family == AF_INET6)
tmp.su_sin6.sin6_scope_id = 0;
if (!getnameinfo((struct sockaddr *)&tmp, tmp.su_len,
hname, sizeof(hname) - 1, NULL, 0,
NI_NUMERICHOST)) {
hname[sizeof(hname) - 1] = 0;
printf(" %s |%d|%s|%d|\r\n",
ispassive ? "EPSV" : "EPRT",
af, hname, htons(tmp.su_port));
}
}
}
#undef UC
} else
printf(" No data connection\r\n");
reply(211, "End of status.");
}
void
fatalerror(char *s)
{
reply(451, "Error in server: %s", s);
reply(221, "Closing connection due to server error.");
dologout(0);
/* NOTREACHED */
}
void
reply(int n, const char *fmt, ...)
{
va_list ap;
(void)printf("%d ", n);
va_start(ap, fmt);
(void)vprintf(fmt, ap);
va_end(ap);
(void)printf("\r\n");
(void)fflush(stdout);
if (ftpdebug) {
syslog(LOG_DEBUG, "<--- %d ", n);
va_start(ap, fmt);
vsyslog(LOG_DEBUG, fmt, ap);
va_end(ap);
}
}
void
lreply(int n, const char *fmt, ...)
{
va_list ap;
(void)printf("%d- ", n);
va_start(ap, fmt);
(void)vprintf(fmt, ap);
va_end(ap);
(void)printf("\r\n");
(void)fflush(stdout);
if (ftpdebug) {
syslog(LOG_DEBUG, "<--- %d- ", n);
va_start(ap, fmt);
vsyslog(LOG_DEBUG, fmt, ap);
va_end(ap);
}
}
static void
ack(char *s)
{
reply(250, "%s command successful.", s);
}
void
nack(char *s)
{
reply(502, "%s command not implemented.", s);
}
/* ARGSUSED */
void
yyerror(char *s)
{
char *cp;
if ((cp = strchr(cbuf,'\n')))
*cp = '\0';
reply(500, "%s: command not understood.", cbuf);
}
void
delete(char *name)
{
struct stat st;
LOGCMD("delete", name);
if (lstat(name, &st) < 0) {
perror_reply(550, name);
return;
}
if (S_ISDIR(st.st_mode)) {
if (rmdir(name) < 0) {
perror_reply(550, name);
return;
}
goto done;
}
if (guest && noguestmod) {
reply(550, "Operation not permitted.");
return;
}
if (unlink(name) < 0) {
perror_reply(550, name);
return;
}
done:
ack("DELE");
}
void
cwd(char *path)
{
if (chdir(path) < 0)
perror_reply(550, path);
else
ack("CWD");
}
void
makedir(char *name)
{
char *s;
LOGCMD("mkdir", name);
if (guest && noguestmkd)
reply(550, "Operation not permitted.");
else if (mkdir(name, 0777) < 0)
perror_reply(550, name);
else {
if ((s = doublequote(name)) == NULL)
fatalerror("Ran out of memory.");
reply(257, "\"%s\" directory created.", s);
free(s);
}
}
void
removedir(char *name)
{
LOGCMD("rmdir", name);
if (rmdir(name) < 0)
perror_reply(550, name);
else
ack("RMD");
}
void
pwd(void)
{
char *s, path[MAXPATHLEN + 1];
if (getcwd(path, sizeof(path)) == NULL)
perror_reply(550, "Get current directory");
else {
if ((s = doublequote(path)) == NULL)
fatalerror("Ran out of memory.");
reply(257, "\"%s\" is current directory.", s);
free(s);
}
}
char *
renamefrom(char *name)
{
struct stat st;
if (guest && noguestmod) {
reply(550, "Operation not permitted.");
return (NULL);
}
if (lstat(name, &st) < 0) {
perror_reply(550, name);
return (NULL);
}
reply(350, "File exists, ready for destination name.");
return (name);
}
void
renamecmd(char *from, char *to)
{
struct stat st;
LOGCMD2("rename", from, to);
if (guest && (stat(to, &st) == 0)) {
reply(550, "%s: permission denied.", to);
return;
}
if (rename(from, to) < 0)
perror_reply(550, "rename");
else
ack("RNTO");
}
static void
dolog(struct sockaddr *who)
{
char who_name[NI_MAXHOST];
realhostname_sa(remotehost, sizeof(remotehost) - 1, who, who->sa_len);
remotehost[sizeof(remotehost) - 1] = 0;
if (getnameinfo(who, who->sa_len,
who_name, sizeof(who_name) - 1, NULL, 0, NI_NUMERICHOST))
*who_name = 0;
who_name[sizeof(who_name) - 1] = 0;
#ifdef SETPROCTITLE
#ifdef VIRTUAL_HOSTING
if (thishost != firsthost)
snprintf(proctitle, sizeof(proctitle), "%s: connected (to %s)",
remotehost, hostname);
else
#endif
snprintf(proctitle, sizeof(proctitle), "%s: connected",
remotehost);
setproctitle("%s", proctitle);
#endif /* SETPROCTITLE */
if (logging) {
#ifdef VIRTUAL_HOSTING
if (thishost != firsthost)
syslog(LOG_INFO, "connection from %s (%s) to %s",
remotehost, who_name, hostname);
else
#endif
syslog(LOG_INFO, "connection from %s (%s)",
remotehost, who_name);
}
}
/*
* Record logout in wtmp file
* and exit with supplied status.
*/
void
dologout(int status)
{
if (logged_in && dowtmp) {
(void) seteuid(0);
#ifdef LOGIN_CAP
setusercontext(NULL, getpwuid(0), 0, LOGIN_SETALL & ~(LOGIN_SETLOGIN |
LOGIN_SETUSER | LOGIN_SETGROUP | LOGIN_SETPATH |
LOGIN_SETENV));
#endif
ftpd_logwtmp(wtmpid, NULL, NULL);
}
/* beware of flushing buffers after a SIGPIPE */
_exit(status);
}
static void
sigurg(int signo)
{
recvurg = 1;
}
static void
maskurg(int flag)
{
int oerrno;
sigset_t sset;
if (!transflag) {
syslog(LOG_ERR, "Internal: maskurg() while no transfer");
return;
}
oerrno = errno;
sigemptyset(&sset);
sigaddset(&sset, SIGURG);
sigprocmask(flag ? SIG_BLOCK : SIG_UNBLOCK, &sset, NULL);
errno = oerrno;
}
static void
flagxfer(int flag)
{
if (flag) {
if (transflag)
syslog(LOG_ERR, "Internal: flagxfer(1): "
"transfer already under way");
transflag = 1;
maskurg(0);
recvurg = 0;
} else {
if (!transflag)
syslog(LOG_ERR, "Internal: flagxfer(0): "
"no active transfer");
maskurg(1);
transflag = 0;
}
}
/*
* Returns 0 if OK to resume or -1 if abort requested.
*/
static int
myoob(void)
{
char *cp;
int ret;
if (!transflag) {
syslog(LOG_ERR, "Internal: myoob() while no transfer");
return (0);
}
cp = tmpline;
ret = get_line(cp, 7, stdin);
if (ret == -1) {
reply(221, "You could at least say goodbye.");
dologout(0);
} else if (ret == -2) {
/* Ignore truncated command. */
return (0);
}
upper(cp);
if (strcmp(cp, "ABOR\r\n") == 0) {
tmpline[0] = '\0';
reply(426, "Transfer aborted. Data connection closed.");
reply(226, "Abort successful.");
return (-1);
}
if (strcmp(cp, "STAT\r\n") == 0) {
tmpline[0] = '\0';
if (file_size != -1)
reply(213, "Status: %jd of %jd bytes transferred.",
(intmax_t)byte_count, (intmax_t)file_size);
else
reply(213, "Status: %jd bytes transferred.",
(intmax_t)byte_count);
}
return (0);
}
/*
* Note: a response of 425 is not mentioned as a possible response to
* the PASV command in RFC959. However, it has been blessed as
* a legitimate response by Jon Postel in a telephone conversation
* with Rick Adams on 25 Jan 89.
*/
void
passive(void)
{
socklen_t len;
int on;
char *p, *a;
if (pdata >= 0) /* close old port if one set */
close(pdata);
pdata = socket(ctrl_addr.su_family, SOCK_STREAM, 0);
if (pdata < 0) {
perror_reply(425, "Can't open passive connection");
return;
}
on = 1;
if (setsockopt(pdata, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
syslog(LOG_WARNING, "pdata setsockopt (SO_REUSEADDR): %m");
(void) seteuid(0);
#ifdef IP_PORTRANGE
if (ctrl_addr.su_family == AF_INET) {
on = restricted_data_ports ? IP_PORTRANGE_HIGH
: IP_PORTRANGE_DEFAULT;
if (setsockopt(pdata, IPPROTO_IP, IP_PORTRANGE,
&on, sizeof(on)) < 0)
goto pasv_error;
}
#endif
#ifdef IPV6_PORTRANGE
if (ctrl_addr.su_family == AF_INET6) {
on = restricted_data_ports ? IPV6_PORTRANGE_HIGH
: IPV6_PORTRANGE_DEFAULT;
if (setsockopt(pdata, IPPROTO_IPV6, IPV6_PORTRANGE,
&on, sizeof(on)) < 0)
goto pasv_error;
}
#endif
pasv_addr = ctrl_addr;
pasv_addr.su_port = 0;
if (bind(pdata, (struct sockaddr *)&pasv_addr, pasv_addr.su_len) < 0)
goto pasv_error;
(void) seteuid(pw->pw_uid);
len = sizeof(pasv_addr);
if (getsockname(pdata, (struct sockaddr *) &pasv_addr, &len) < 0)
goto pasv_error;
if (listen(pdata, 1) < 0)
goto pasv_error;
if (pasv_addr.su_family == AF_INET)
a = (char *) &pasv_addr.su_sin.sin_addr;
else if (pasv_addr.su_family == AF_INET6 &&
IN6_IS_ADDR_V4MAPPED(&pasv_addr.su_sin6.sin6_addr))
a = (char *) &pasv_addr.su_sin6.sin6_addr.s6_addr[12];
else
goto pasv_error;
p = (char *) &pasv_addr.su_port;
#define UC(b) (((int) b) & 0xff)
reply(227, "Entering Passive Mode (%d,%d,%d,%d,%d,%d)", UC(a[0]),
UC(a[1]), UC(a[2]), UC(a[3]), UC(p[0]), UC(p[1]));
return;
pasv_error:
(void) seteuid(pw->pw_uid);
(void) close(pdata);
pdata = -1;
perror_reply(425, "Can't open passive connection");
return;
}
/*
* Long Passive defined in RFC 1639.
* 228 Entering Long Passive Mode
* (af, hal, h1, h2, h3,..., pal, p1, p2...)
*/
void
long_passive(char *cmd, int pf)
{
socklen_t len;
int on;
char *p, *a;
if (pdata >= 0) /* close old port if one set */
close(pdata);
if (pf != PF_UNSPEC) {
if (ctrl_addr.su_family != pf) {
switch (ctrl_addr.su_family) {
case AF_INET:
pf = 1;
break;
case AF_INET6:
pf = 2;
break;
default:
pf = 0;
break;
}
/*
* XXX
* only EPRT/EPSV ready clients will understand this
*/
if (strcmp(cmd, "EPSV") == 0 && pf) {
reply(522, "Network protocol mismatch, "
"use (%d)", pf);
} else
reply(501, "Network protocol mismatch."); /*XXX*/
return;
}
}
pdata = socket(ctrl_addr.su_family, SOCK_STREAM, 0);
if (pdata < 0) {
perror_reply(425, "Can't open passive connection");
return;
}
on = 1;
if (setsockopt(pdata, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
syslog(LOG_WARNING, "pdata setsockopt (SO_REUSEADDR): %m");
(void) seteuid(0);
pasv_addr = ctrl_addr;
pasv_addr.su_port = 0;
len = pasv_addr.su_len;
#ifdef IP_PORTRANGE
if (ctrl_addr.su_family == AF_INET) {
on = restricted_data_ports ? IP_PORTRANGE_HIGH
: IP_PORTRANGE_DEFAULT;
if (setsockopt(pdata, IPPROTO_IP, IP_PORTRANGE,
&on, sizeof(on)) < 0)
goto pasv_error;
}
#endif
#ifdef IPV6_PORTRANGE
if (ctrl_addr.su_family == AF_INET6) {
on = restricted_data_ports ? IPV6_PORTRANGE_HIGH
: IPV6_PORTRANGE_DEFAULT;
if (setsockopt(pdata, IPPROTO_IPV6, IPV6_PORTRANGE,
&on, sizeof(on)) < 0)
goto pasv_error;
}
#endif
if (bind(pdata, (struct sockaddr *)&pasv_addr, len) < 0)
goto pasv_error;
(void) seteuid(pw->pw_uid);
if (getsockname(pdata, (struct sockaddr *) &pasv_addr, &len) < 0)
goto pasv_error;
if (listen(pdata, 1) < 0)
goto pasv_error;
#define UC(b) (((int) b) & 0xff)
if (strcmp(cmd, "LPSV") == 0) {
p = (char *)&pasv_addr.su_port;
switch (pasv_addr.su_family) {
case AF_INET:
a = (char *) &pasv_addr.su_sin.sin_addr;
v4_reply:
reply(228,
"Entering Long Passive Mode (%d,%d,%d,%d,%d,%d,%d,%d,%d)",
4, 4, UC(a[0]), UC(a[1]), UC(a[2]), UC(a[3]),
2, UC(p[0]), UC(p[1]));
return;
case AF_INET6:
if (IN6_IS_ADDR_V4MAPPED(&pasv_addr.su_sin6.sin6_addr)) {
a = (char *) &pasv_addr.su_sin6.sin6_addr.s6_addr[12];
goto v4_reply;
}
a = (char *) &pasv_addr.su_sin6.sin6_addr;
reply(228,
"Entering Long Passive Mode "
"(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)",
6, 16, UC(a[0]), UC(a[1]), UC(a[2]), UC(a[3]),
UC(a[4]), UC(a[5]), UC(a[6]), UC(a[7]),
UC(a[8]), UC(a[9]), UC(a[10]), UC(a[11]),
UC(a[12]), UC(a[13]), UC(a[14]), UC(a[15]),
2, UC(p[0]), UC(p[1]));
return;
}
} else if (strcmp(cmd, "EPSV") == 0) {
switch (pasv_addr.su_family) {
case AF_INET:
case AF_INET6:
reply(229, "Entering Extended Passive Mode (|||%d|)",
ntohs(pasv_addr.su_port));
return;
}
} else {
/* more proper error code? */
}
pasv_error:
(void) seteuid(pw->pw_uid);
(void) close(pdata);
pdata = -1;
perror_reply(425, "Can't open passive connection");
return;
}
/*
* Generate unique name for file with basename "local"
* and open the file in order to avoid possible races.
* Try "local" first, then "local.1", "local.2" etc, up to "local.99".
* Return descriptor to the file, set "name" to its name.
*
* Generates failure reply on error.
*/
static int
guniquefd(char *local, char **name)
{
static char new[MAXPATHLEN];
struct stat st;
char *cp;
int count;
int fd;
cp = strrchr(local, '/');
if (cp)
*cp = '\0';
if (stat(cp ? local : ".", &st) < 0) {
perror_reply(553, cp ? local : ".");
return (-1);
}
if (cp) {
/*
* Let not overwrite dirname with counter suffix.
* -4 is for /nn\0
* In this extreme case dot won't be put in front of suffix.
*/
if (strlen(local) > sizeof(new) - 4) {
reply(553, "Pathname too long.");
return (-1);
}
*cp = '/';
}
/* -4 is for the .nn<null> we put on the end below */
(void) snprintf(new, sizeof(new) - 4, "%s", local);
cp = new + strlen(new);
/*
* Don't generate dotfile unless requested explicitly.
* This covers the case when basename gets truncated off
* by buffer size.
*/
if (cp > new && cp[-1] != '/')
*cp++ = '.';
for (count = 0; count < 100; count++) {
/* At count 0 try unmodified name */
if (count)
(void)sprintf(cp, "%d", count);
if ((fd = open(count ? new : local,
O_RDWR | O_CREAT | O_EXCL, 0666)) >= 0) {
*name = count ? new : local;
return (fd);
}
if (errno != EEXIST) {
perror_reply(553, count ? new : local);
return (-1);
}
}
reply(452, "Unique file name cannot be created.");
return (-1);
}
/*
* Format and send reply containing system error number.
*/
void
perror_reply(int code, char *string)
{
reply(code, "%s: %s.", string, strerror(errno));
}
static char *onefile[] = {
"",
0
};
void
send_file_list(char *whichf)
{
struct stat st;
DIR *dirp = NULL;
struct dirent *dir;
FILE *dout = NULL;
char **dirlist, *dirname;
int simple = 0;
int freeglob = 0;
glob_t gl;
if (strpbrk(whichf, "~{[*?") != NULL) {
int flags = GLOB_BRACE|GLOB_NOCHECK|GLOB_TILDE;
memset(&gl, 0, sizeof(gl));
gl.gl_matchc = MAXGLOBARGS;
flags |= GLOB_LIMIT;
freeglob = 1;
if (glob(whichf, flags, 0, &gl)) {
reply(550, "No matching files found.");
goto out;
} else if (gl.gl_pathc == 0) {
errno = ENOENT;
perror_reply(550, whichf);
goto out;
}
dirlist = gl.gl_pathv;
} else {
onefile[0] = whichf;
dirlist = onefile;
simple = 1;
}
while ((dirname = *dirlist++)) {
if (stat(dirname, &st) < 0) {
/*
* If user typed "ls -l", etc, and the client
* used NLST, do what the user meant.
*/
if (dirname[0] == '-' && *dirlist == NULL &&
dout == NULL)
retrieve(_PATH_LS " %s", dirname);
else
perror_reply(550, whichf);
goto out;
}
if (S_ISREG(st.st_mode)) {
if (dout == NULL) {
dout = dataconn("file list", -1, "w");
if (dout == NULL)
goto out;
STARTXFER;
}
START_UNSAFE;
fprintf(dout, "%s%s\n", dirname,
type == TYPE_A ? "\r" : "");
END_UNSAFE;
if (ferror(dout))
goto data_err;
byte_count += strlen(dirname) +
(type == TYPE_A ? 2 : 1);
CHECKOOB(goto abrt);
continue;
} else if (!S_ISDIR(st.st_mode))
continue;
if ((dirp = opendir(dirname)) == NULL)
continue;
while ((dir = readdir(dirp)) != NULL) {
char nbuf[MAXPATHLEN];
CHECKOOB(goto abrt);
if (dir->d_name[0] == '.' && dir->d_namlen == 1)
continue;
if (dir->d_name[0] == '.' && dir->d_name[1] == '.' &&
dir->d_namlen == 2)
continue;
snprintf(nbuf, sizeof(nbuf),
"%s/%s", dirname, dir->d_name);
/*
* We have to do a stat to insure it's
* not a directory or special file.
*/
if (simple || (stat(nbuf, &st) == 0 &&
S_ISREG(st.st_mode))) {
if (dout == NULL) {
dout = dataconn("file list", -1, "w");
if (dout == NULL)
goto out;
STARTXFER;
}
START_UNSAFE;
if (nbuf[0] == '.' && nbuf[1] == '/')
fprintf(dout, "%s%s\n", &nbuf[2],
type == TYPE_A ? "\r" : "");
else
fprintf(dout, "%s%s\n", nbuf,
type == TYPE_A ? "\r" : "");
END_UNSAFE;
if (ferror(dout))
goto data_err;
byte_count += strlen(nbuf) +
(type == TYPE_A ? 2 : 1);
CHECKOOB(goto abrt);
}
}
(void) closedir(dirp);
dirp = NULL;
}
if (dout == NULL)
reply(550, "No files found.");
else if (ferror(dout))
data_err: perror_reply(550, "Data connection");
else
reply(226, "Transfer complete.");
out:
if (dout) {
ENDXFER;
abrt:
(void) fclose(dout);
data = -1;
pdata = -1;
}
if (dirp)
(void) closedir(dirp);
if (freeglob) {
freeglob = 0;
globfree(&gl);
}
}
void
reapchild(int signo)
{
while (waitpid(-1, NULL, WNOHANG) > 0);
}
static void
appendf(char **strp, char *fmt, ...)
{
va_list ap;
char *ostr, *p;
va_start(ap, fmt);
vasprintf(&p, fmt, ap);
va_end(ap);
if (p == NULL)
fatalerror("Ran out of memory.");
if (*strp == NULL)
*strp = p;
else {
ostr = *strp;
asprintf(strp, "%s%s", ostr, p);
if (*strp == NULL)
fatalerror("Ran out of memory.");
free(ostr);
}
}
static void
logcmd(char *cmd, char *file1, char *file2, off_t cnt)
{
char *msg = NULL;
char wd[MAXPATHLEN + 1];
if (logging <= 1)
return;
if (getcwd(wd, sizeof(wd) - 1) == NULL)
strcpy(wd, strerror(errno));
appendf(&msg, "%s", cmd);
if (file1)
appendf(&msg, " %s", file1);
if (file2)
appendf(&msg, " %s", file2);
if (cnt >= 0)
appendf(&msg, " = %jd bytes", (intmax_t)cnt);
appendf(&msg, " (wd: %s", wd);
if (guest || dochroot)
appendf(&msg, "; chrooted");
appendf(&msg, ")");
syslog(LOG_INFO, "%s", msg);
free(msg);
}
static void
logxfer(char *name, off_t size, time_t start)
{
char buf[MAXPATHLEN + 1024];
char path[MAXPATHLEN + 1];
time_t now;
if (statfd >= 0) {
time(&now);
if (realpath(name, path) == NULL) {
syslog(LOG_NOTICE, "realpath failed on %s: %m", path);
return;
}
snprintf(buf, sizeof(buf), "%.20s!%s!%s!%s!%jd!%ld\n",
ctime(&now)+4, ident, remotehost,
path, (intmax_t)size,
(long)(now - start + (now == start)));
write(statfd, buf, strlen(buf));
}
}
static char *
doublequote(char *s)
{
int n;
char *p, *s2;
for (p = s, n = 0; *p; p++)
if (*p == '"')
n++;
if ((s2 = malloc(p - s + n + 1)) == NULL)
return (NULL);
for (p = s2; *s; s++, p++) {
if ((*p = *s) == '"')
*(++p) = '"';
}
*p = '\0';
return (s2);
}
/* setup server socket for specified address family */
/* if af is PF_UNSPEC more than one socket may be returned */
/* the returned list is dynamically allocated, so caller needs to free it */
static int *
socksetup(int af, char *bindname, const char *bindport)
{
struct addrinfo hints, *res, *r;
int error, maxs, *s, *socks;
const int on = 1;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_PASSIVE;
hints.ai_family = af;
hints.ai_socktype = SOCK_STREAM;
error = getaddrinfo(bindname, bindport, &hints, &res);
if (error) {
syslog(LOG_ERR, "%s", gai_strerror(error));
if (error == EAI_SYSTEM)
syslog(LOG_ERR, "%s", strerror(errno));
return NULL;
}
/* Count max number of sockets we may open */
for (maxs = 0, r = res; r; r = r->ai_next, maxs++)
;
socks = malloc((maxs + 1) * sizeof(int));
if (!socks) {
freeaddrinfo(res);
syslog(LOG_ERR, "couldn't allocate memory for sockets");
return NULL;
}
*socks = 0; /* num of sockets counter at start of array */
s = socks + 1;
for (r = res; r; r = r->ai_next) {
*s = socket(r->ai_family, r->ai_socktype, r->ai_protocol);
if (*s < 0) {
syslog(LOG_DEBUG, "control socket: %m");
continue;
}
if (setsockopt(*s, SOL_SOCKET, SO_REUSEADDR,
&on, sizeof(on)) < 0)
syslog(LOG_WARNING,
"control setsockopt (SO_REUSEADDR): %m");
if (r->ai_family == AF_INET6) {
if (setsockopt(*s, IPPROTO_IPV6, IPV6_V6ONLY,
&on, sizeof(on)) < 0)
syslog(LOG_WARNING,
"control setsockopt (IPV6_V6ONLY): %m");
}
if (bind(*s, r->ai_addr, r->ai_addrlen) < 0) {
syslog(LOG_DEBUG, "control bind: %m");
close(*s);
continue;
}
(*socks)++;
s++;
}
if (res)
freeaddrinfo(res);
if (*socks == 0) {
syslog(LOG_ERR, "control socket: Couldn't bind to any socket");
free(socks);
return NULL;
}
return(socks);
}