-/* tftpd.c 4.4 82/11/14 */
+/*
+ * Copyright (c) 1983, 1993
+ * The Regents of the University of California. All rights reserved.
+ *
+ * %sccs.include.redist.c%
+ */
+
+#ifndef lint
+static char copyright[] =
+"@(#) Copyright (c) 1983, 1993\n\
+ The Regents of the University of California. All rights reserved.\n";
+#endif /* not lint */
+
+#ifndef lint
+static char sccsid[] = "@(#)tftpd.c 8.1 (Berkeley) %G%";
+#endif /* not lint */
/*
* Trivial file transfer protocol server.
+ *
+ * This version includes many modifications by Jim Guyton
+ * <guyton@rand-unix>.
*/
-#include <sys/types.h>
-#include <sys/socket.h>
+
+#include <sys/param.h>
#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
#include <netinet/in.h>
+#include <arpa/tftp.h>
+#include <arpa/inet.h>
-#include <signal.h>
-#include <stat.h>
-#include <stdio.h>
-#include <wait.h>
-#include <errno.h>
#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
#include <netdb.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <unistd.h>
-#include "tftp.h"
+#include "tftpsubs.h"
-extern int errno;
-struct sockaddr_in sin = { AF_INET };
-int f;
-char buf[BUFSIZ];
+#define TIMEOUT 5
+int peer;
+int rexmtval = TIMEOUT;
+int maxtimeout = 5*TIMEOUT;
+
+#define PKTSIZE SEGSIZE+4
+char buf[PKTSIZE];
+char ackbuf[PKTSIZE];
+struct sockaddr_in from;
+int fromlen;
+
+void tftp __P((struct tftphdr *, int));
+
+/*
+ * Null-terminated directory prefix list for absolute pathname requests and
+ * search list for relative pathname requests.
+ *
+ * MAXDIRS should be at least as large as the number of arguments that
+ * inetd allows (currently 20).
+ */
+#define MAXDIRS 20
+static struct dirlist {
+ char *name;
+ int len;
+} dirs[MAXDIRS+1];
+static int suppress_naks;
+static int logging;
+
+static char *errtomsg __P((int));
+static void nak __P((int));
+static char *verifyhost __P((struct sockaddr_in *));
+
+int
main(argc, argv)
+ int argc;
char *argv[];
{
- union wait status;
- struct sockaddr_in from;
register struct tftphdr *tp;
register int n;
- struct servent *sp;
+ int ch, on;
+ struct sockaddr_in sin;
+
+ openlog("tftpd", LOG_PID, LOG_FTP);
+ while ((ch = getopt(argc, argv, "ln")) != EOF) {
+ switch (ch) {
+ case 'l':
+ logging = 1;
+ break;
+ case 'n':
+ suppress_naks = 1;
+ break;
+ default:
+ syslog(LOG_WARNING, "ignoring unknown option -%c", ch);
+ }
+ }
+ if (optind < argc) {
+ struct dirlist *dirp;
- sp = getservbyname("tftp", "udp");
- if (sp == 0) {
- fprintf(stderr, "tftpd: udp/tftp: unknown service\n");
+ /* Get list of directory prefixes. Skip relative pathnames. */
+ for (dirp = dirs; optind < argc && dirp < &dirs[MAXDIRS];
+ optind++) {
+ if (argv[optind][0] == '/') {
+ dirp->name = argv[optind];
+ dirp->len = strlen(dirp->name);
+ dirp++;
+ }
+ }
+ }
+
+ on = 1;
+ if (ioctl(0, FIONBIO, &on) < 0) {
+ syslog(LOG_ERR, "ioctl(FIONBIO): %m\n");
exit(1);
}
- sin.sin_port = htons((u_short)sp->s_port);
-#ifndef DEBUG
- if (fork())
- exit(0);
- for (f = 0; f < 10; f++)
- (void) close(f);
- (void) open("/", 0);
- (void) dup2(0, 1);
- (void) dup2(0, 2);
- { int t = open("/dev/tty", 2);
- if (t >= 0) {
- ioctl(t, TIOCNOTTY, (char *)0);
- (void) close(t);
- }
+ fromlen = sizeof (from);
+ n = recvfrom(0, buf, sizeof (buf), 0,
+ (struct sockaddr *)&from, &fromlen);
+ if (n < 0) {
+ syslog(LOG_ERR, "recvfrom: %m\n");
+ exit(1);
}
-#endif
- for (;;) {
- int fromlen;
-
- f = socket(0, SOCK_DGRAM, 0, 0);
- if (f < 0) {
- perror("tftpd: socket");
- close(f);
- sleep(5);
- continue;
- }
- while (bind(f, (caddr_t)&sin, sizeof (sin), 0) < 0) {
- perror("tftpd: bind");
- close(f);
- sleep(5);
- continue;
+ /*
+ * Now that we have read the message out of the UDP
+ * socket, we fork and exit. Thus, inetd will go back
+ * to listening to the tftp port, and the next request
+ * to come in will start up a new instance of tftpd.
+ *
+ * We do this so that inetd can run tftpd in "wait" mode.
+ * The problem with tftpd running in "nowait" mode is that
+ * inetd may get one or more successful "selects" on the
+ * tftp port before we do our receive, so more than one
+ * instance of tftpd may be started up. Worse, if tftpd
+ * break before doing the above "recvfrom", inetd would
+ * spawn endless instances, clogging the system.
+ */
+ {
+ int pid;
+ int i, j;
+
+ for (i = 1; i < 20; i++) {
+ pid = fork();
+ if (pid < 0) {
+ sleep(i);
+ /*
+ * flush out to most recently sent request.
+ *
+ * This may drop some request, but those
+ * will be resent by the clients when
+ * they timeout. The positive effect of
+ * this flush is to (try to) prevent more
+ * than one tftpd being started up to service
+ * a single request from a single client.
+ */
+ j = sizeof from;
+ i = recvfrom(0, buf, sizeof (buf), 0,
+ (struct sockaddr *)&from, &j);
+ if (i > 0) {
+ n = i;
+ fromlen = j;
+ }
+ } else {
+ break;
+ }
}
-again:
- fromlen = sizeof (from);
- n = recvfrom(f, buf, sizeof (buf), (caddr_t)&from, &fromlen, 0);
- if (n <= 0) {
- if (n < 0)
- perror("tftpd: recvfrom");
- goto again;
+ if (pid < 0) {
+ syslog(LOG_ERR, "fork: %m\n");
+ exit(1);
+ } else if (pid != 0) {
+ exit(0);
}
- tp = (struct tftphdr *)buf;
- tp->th_opcode = ntohs(tp->th_opcode);
- if (tp->th_opcode == RRQ || tp->th_opcode == WRQ)
- if (fork() == 0)
- tftp(&from, tp, n);
- (void) close(f);
- while (wait3(status, WNOHANG, 0) > 0)
- continue;
}
+ from.sin_family = AF_INET;
+ alarm(0);
+ close(0);
+ close(1);
+ peer = socket(AF_INET, SOCK_DGRAM, 0);
+ if (peer < 0) {
+ syslog(LOG_ERR, "socket: %m\n");
+ exit(1);
+ }
+ memset(&sin, 0, sizeof(sin));
+ sin.sin_family = AF_INET;
+ if (bind(peer, (struct sockaddr *)&sin, sizeof (sin)) < 0) {
+ syslog(LOG_ERR, "bind: %m\n");
+ exit(1);
+ }
+ if (connect(peer, (struct sockaddr *)&from, sizeof(from)) < 0) {
+ syslog(LOG_ERR, "connect: %m\n");
+ exit(1);
+ }
+ tp = (struct tftphdr *)buf;
+ tp->th_opcode = ntohs(tp->th_opcode);
+ if (tp->th_opcode == RRQ || tp->th_opcode == WRQ)
+ tftp(tp, n);
+ exit(1);
}
-int validate_access();
-int sendfile(), recvfile();
+struct formats;
+int validate_access __P((char **, int));
+void sendfile __P((struct formats *));
+void recvfile __P((struct formats *));
struct formats {
char *f_mode;
- int (*f_validate)();
- int (*f_send)();
- int (*f_recv)();
+ int (*f_validate) __P((char **, int));
+ void (*f_send) __P((struct formats *));
+ void (*f_recv) __P((struct formats *));
+ int f_convert;
} formats[] = {
- { "netascii", validate_access, sendfile, recvfile },
- { "octet", validate_access, sendfile, recvfile },
+ { "netascii", validate_access, sendfile, recvfile, 1 },
+ { "octet", validate_access, sendfile, recvfile, 0 },
#ifdef notdef
- { "mail", validate_user, sendmail, recvmail },
+ { "mail", validate_user, sendmail, recvmail, 1 },
#endif
{ 0 }
};
-int fd; /* file being transferred */
-
/*
* Handle initial connection protocol.
*/
-tftp(client, tp, size)
- struct sockaddr_in *client;
+void
+tftp(tp, size)
struct tftphdr *tp;
int size;
{
register struct formats *pf;
char *filename, *mode;
- if (connect(f, (caddr_t)client, sizeof (*client), 0) < 0) {
- perror("connect");
- exit(1);
- }
filename = cp = tp->th_stuff;
again:
while (cp < buf + size) {
nak(EBADOP);
exit(1);
}
- ecode = (*pf->f_validate)(filename, client, tp->th_opcode);
+ ecode = (*pf->f_validate)(&filename, tp->th_opcode);
+ if (logging) {
+ syslog(LOG_INFO, "%s: %s request for %s: %s",
+ verifyhost(&from),
+ tp->th_opcode == WRQ ? "write" : "read",
+ filename, errtomsg(ecode));
+ }
if (ecode) {
+ /*
+ * Avoid storms of naks to a RRQ broadcast for a relative
+ * bootfile pathname from a diskless Sun.
+ */
+ if (suppress_naks && *filename != '/' && ecode == ENOTFOUND)
+ exit(0);
nak(ecode);
exit(1);
}
exit(0);
}
+
+FILE *file;
+
/*
* Validate file access. Since we
* have no uid or gid, for now require
* file to exist and be publicly
* readable/writable.
+ * If we were invoked with arguments
+ * from inetd then the file must also be
+ * in one of the given directory prefixes.
* Note also, full path name must be
* given as we have no login directory.
*/
-validate_access(file, client, mode)
- char *file;
- struct sockaddr_in *client;
+int
+validate_access(filep, mode)
+ char **filep;
int mode;
{
struct stat stbuf;
+ int fd;
+ struct dirlist *dirp;
+ static char pathname[MAXPATHLEN];
+ char *filename = *filep;
- if (*file != '/')
+ /*
+ * Prevent tricksters from getting around the directory restrictions
+ */
+ if (strstr(filename, "/../"))
return (EACCESS);
- if (stat(file, &stbuf) < 0)
- return (errno == ENOENT ? ENOTFOUND : EACCESS);
- if (mode == RRQ) {
- if ((stbuf.st_mode&(S_IREAD >> 6)) == 0)
+
+ if (*filename == '/') {
+ /*
+ * Allow the request if it's in one of the approved locations.
+ * Special case: check the null prefix ("/") by looking
+ * for length = 1 and relying on the arg. processing that
+ * it's a /.
+ */
+ for (dirp = dirs; dirp->name != NULL; dirp++) {
+ if (dirp->len == 1 ||
+ (!strncmp(filename, dirp->name, dirp->len) &&
+ filename[dirp->len] == '/'))
+ break;
+ }
+ /* If directory list is empty, allow access to any file */
+ if (dirp->name == NULL && dirp != dirs)
return (EACCESS);
+ if (stat(filename, &stbuf) < 0)
+ return (errno == ENOENT ? ENOTFOUND : EACCESS);
+ if ((stbuf.st_mode & S_IFMT) != S_IFREG)
+ return (ENOTFOUND);
+ if (mode == RRQ) {
+ if ((stbuf.st_mode & S_IROTH) == 0)
+ return (EACCESS);
+ } else {
+ if ((stbuf.st_mode & S_IWOTH) == 0)
+ return (EACCESS);
+ }
} else {
- if ((stbuf.st_mode&(S_IWRITE >> 6)) == 0)
+ int err;
+
+ /*
+ * Relative file name: search the approved locations for it.
+ * Don't allow write requests or ones that avoid directory
+ * restrictions.
+ */
+
+ if (mode != RRQ || !strncmp(filename, "../", 3))
return (EACCESS);
+
+ /*
+ * If the file exists in one of the directories and isn't
+ * readable, continue looking. However, change the error code
+ * to give an indication that the file exists.
+ */
+ err = ENOTFOUND;
+ for (dirp = dirs; dirp->name != NULL; dirp++) {
+ sprintf(pathname, "%s/%s", dirp->name, filename);
+ if (stat(pathname, &stbuf) == 0 &&
+ (stbuf.st_mode & S_IFMT) == S_IFREG) {
+ if ((stbuf.st_mode & S_IROTH) != 0) {
+ break;
+ }
+ err = EACCESS;
+ }
+ }
+ if (dirp->name == NULL)
+ return (err);
+ *filep = filename = pathname;
}
- fd = open(file, mode == RRQ ? 0 : 1);
+ fd = open(filename, mode == RRQ ? 0 : 1);
if (fd < 0)
return (errno + 100);
+ file = fdopen(fd, (mode == RRQ)? "r":"w");
+ if (file == NULL) {
+ return errno+100;
+ }
return (0);
}
-int timeout;
+int timeout;
+jmp_buf timeoutbuf;
+void
timer()
{
- timeout += TIMEOUT;
- if (timeout >= MAXTIMEOUT)
+
+ timeout += rexmtval;
+ if (timeout >= maxtimeout)
exit(1);
- alarm(TIMEOUT);
+ longjmp(timeoutbuf, 1);
}
/*
* Send the requested file.
*/
+void
sendfile(pf)
- struct format *pf;
+ struct formats *pf;
{
- register struct tftphdr *tp;
- register int block = 1, size, n;
+ struct tftphdr *dp, *r_init();
+ register struct tftphdr *ap; /* ack packet */
+ register int size, n;
+ volatile int block;
- sigset(SIGALRM, timer);
- tp = (struct tftphdr *)buf;
+ signal(SIGALRM, timer);
+ dp = r_init();
+ ap = (struct tftphdr *)ackbuf;
+ block = 1;
do {
- size = read(fd, tp->th_data, SEGSIZE);
+ size = readit(file, &dp, pf->f_convert);
if (size < 0) {
nak(errno + 100);
- break;
+ goto abort;
}
- tp->th_opcode = htons((u_short)DATA);
- tp->th_block = htons((u_short)block);
+ dp->th_opcode = htons((u_short)DATA);
+ dp->th_block = htons((u_short)block);
timeout = 0;
- alarm(TIMEOUT);
-rexmt:
- if (write(f, buf, size + 4) != size + 4) {
- perror("send");
- break;
+ (void)setjmp(timeoutbuf);
+
+send_data:
+ if (send(peer, dp, size + 4, 0) != size + 4) {
+ syslog(LOG_ERR, "tftpd: write: %m\n");
+ goto abort;
}
-again:
- n = read(f, buf, sizeof (buf));
- if (n <= 0) {
- if (n == 0)
- goto again;
- if (errno == EINTR)
- goto rexmt;
+ read_ahead(file, pf->f_convert);
+ for ( ; ; ) {
+ alarm(rexmtval); /* read the ack */
+ n = recv(peer, ackbuf, sizeof (ackbuf), 0);
alarm(0);
- perror("receive");
- break;
+ if (n < 0) {
+ syslog(LOG_ERR, "tftpd: read: %m\n");
+ goto abort;
+ }
+ ap->th_opcode = ntohs((u_short)ap->th_opcode);
+ ap->th_block = ntohs((u_short)ap->th_block);
+
+ if (ap->th_opcode == ERROR)
+ goto abort;
+
+ if (ap->th_opcode == ACK) {
+ if (ap->th_block == block)
+ break;
+ /* Re-synchronize with the other side */
+ (void) synchnet(peer);
+ if (ap->th_block == (block -1))
+ goto send_data;
+ }
+
}
- alarm(0);
-#if vax || pdp11
- tp->th_opcode = ntohs((u_short)tp->th_opcode);
- tp->th_block = ntohs((u_short)tp->th_block);
-#endif
- if (tp->th_opcode == ERROR)
- break;
- if (tp->th_opcode != ACK || tp->th_block != block)
- goto again;
block++;
} while (size == SEGSIZE);
- (void) close(fd);
+abort:
+ (void) fclose(file);
+}
+
+void
+justquit()
+{
+ exit(0);
}
+
/*
* Receive a file.
*/
+void
recvfile(pf)
- struct format *pf;
+ struct formats *pf;
{
- register struct tftphdr *tp;
- register int block = 0, n, size;
+ struct tftphdr *dp, *w_init();
+ register struct tftphdr *ap; /* ack buffer */
+ register int n, size;
+ volatile int block;
- sigset(SIGALRM, timer);
- tp = (struct tftphdr *)buf;
+ signal(SIGALRM, timer);
+ dp = w_init();
+ ap = (struct tftphdr *)ackbuf;
+ block = 0;
do {
timeout = 0;
- alarm(TIMEOUT);
- tp->th_opcode = htons((u_short)ACK);
- tp->th_block = htons((u_short)block);
+ ap->th_opcode = htons((u_short)ACK);
+ ap->th_block = htons((u_short)block);
block++;
-rexmt:
- if (write(f, buf, 4) != 4) {
- perror("ack");
- break;
+ (void) setjmp(timeoutbuf);
+send_ack:
+ if (send(peer, ackbuf, 4, 0) != 4) {
+ syslog(LOG_ERR, "tftpd: write: %m\n");
+ goto abort;
}
-again:
- n = read(f, buf, sizeof (buf));
- if (n <= 0) {
- if (n == 0)
- goto again;
- if (errno == EINTR)
- goto rexmt;
+ write_behind(file, pf->f_convert);
+ for ( ; ; ) {
+ alarm(rexmtval);
+ n = recv(peer, dp, PKTSIZE, 0);
alarm(0);
- perror("receive");
- break;
+ if (n < 0) { /* really? */
+ syslog(LOG_ERR, "tftpd: read: %m\n");
+ goto abort;
+ }
+ dp->th_opcode = ntohs((u_short)dp->th_opcode);
+ dp->th_block = ntohs((u_short)dp->th_block);
+ if (dp->th_opcode == ERROR)
+ goto abort;
+ if (dp->th_opcode == DATA) {
+ if (dp->th_block == block) {
+ break; /* normal */
+ }
+ /* Re-synchronize with the other side */
+ (void) synchnet(peer);
+ if (dp->th_block == (block-1))
+ goto send_ack; /* rexmit */
+ }
}
- alarm(0);
-#if vax || pdp11
- tp->th_opcode = ntohs((u_short)tp->th_opcode);
- tp->th_block = ntohs((u_short)tp->th_block);
-#endif
- if (tp->th_opcode == ERROR)
- break;
- if (tp->th_opcode != DATA || block != tp->th_block)
- goto again;
- size = write(fd, tp->th_data, n - 4);
- if (size < 0) {
- nak(errno + 100);
- break;
+ /* size = write(file, dp->th_data, n - 4); */
+ size = writeit(file, &dp, n - 4, pf->f_convert);
+ if (size != (n-4)) { /* ahem */
+ if (size < 0) nak(errno + 100);
+ else nak(ENOSPACE);
+ goto abort;
}
} while (size == SEGSIZE);
- tp->th_opcode = htons((u_short)ACK);
- tp->th_block = htons((u_short)(block));
- (void) write(f, buf, 4);
- (void) close(fd);
+ write_behind(file, pf->f_convert);
+ (void) fclose(file); /* close data file */
+
+ ap->th_opcode = htons((u_short)ACK); /* send the "final" ack */
+ ap->th_block = htons((u_short)(block));
+ (void) send(peer, ackbuf, 4, 0);
+
+ signal(SIGALRM, justquit); /* just quit on timeout */
+ alarm(rexmtval);
+ n = recv(peer, buf, sizeof (buf), 0); /* normally times out and quits */
+ alarm(0);
+ if (n >= 4 && /* if read some data */
+ dp->th_opcode == DATA && /* and got a data block */
+ block == dp->th_block) { /* then my last ack was lost */
+ (void) send(peer, ackbuf, 4, 0); /* resend final ack */
+ }
+abort:
+ return;
}
struct errmsg {
{ -1, 0 }
};
+static char *
+errtomsg(error)
+ int error;
+{
+ static char buf[20];
+ register struct errmsg *pe;
+ if (error == 0)
+ return "success";
+ for (pe = errmsgs; pe->e_code >= 0; pe++)
+ if (pe->e_code == error)
+ return pe->e_msg;
+ sprintf(buf, "error %d", error);
+ return buf;
+}
+
/*
* Send a nak packet (error message).
* Error code passed in is one of the
* standard TFTP codes, or a UNIX errno
* offset by 100.
*/
+static void
nak(error)
int error;
{
register struct tftphdr *tp;
int length;
register struct errmsg *pe;
- extern char *sys_errlist[];
tp = (struct tftphdr *)buf;
tp->th_opcode = htons((u_short)ERROR);
for (pe = errmsgs; pe->e_code >= 0; pe++)
if (pe->e_code == error)
break;
- if (pe->e_code < 0)
- pe->e_msg = sys_errlist[error - 100];
+ if (pe->e_code < 0) {
+ pe->e_msg = strerror(error - 100);
+ tp->th_code = EUNDEF; /* set 'undef' errorcode */
+ }
strcpy(tp->th_msg, pe->e_msg);
length = strlen(pe->e_msg);
tp->th_msg[length] = '\0';
length += 5;
- if (write(f, buf, length) != length)
- perror("nak");
+ if (send(peer, buf, length, 0) != length)
+ syslog(LOG_ERR, "nak: %m\n");
+}
+
+static char *
+verifyhost(fromp)
+ struct sockaddr_in *fromp;
+{
+ struct hostent *hp;
+
+ hp = gethostbyaddr((char *)&fromp->sin_addr, sizeof (fromp->sin_addr),
+ fromp->sin_family);
+ if (hp)
+ return hp->h_name;
+ else
+ return inet_ntoa(fromp->sin_addr);
}