/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 * Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * Copyright (c) 1983, 1984, 1985, 1986, 1987, 1988, 1989 AT&T
 * All Rights Reserved.
 */

/*
 * University Copyright- Copyright (c) 1982, 1986, 1988
 * The Regents of the University of California.
 * All Rights Reserved.
 *
 * University Acknowledgment- Portions of this document are derived from
 * software developed by the University of California, Berkeley, and its
 * contributors.
 */

/*
 * Trivial file transfer protocol server.  A top level process runs in
 * an infinite loop fielding new TFTP requests.  A child process,
 * communicating via a pipe with the top level process, sends delayed
 * NAKs for those that we can't handle.  A new child process is created
 * to service each request that we can handle.  The top level process
 * exits after a period of time during which no new requests are
 * received.
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/time.h>

#include <netinet/in.h>

#include <arpa/inet.h>
#include <dirent.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#include <netdb.h>
#include <setjmp.h>
#include <syslog.h>
#include <sys/param.h>
#include <fcntl.h>
#include <pwd.h>
#include <string.h>
#include <priv_utils.h>
#include "tftpcommon.h"

#define	TIMEOUT		5
#define	DELAY_SECS	3
#define	DALLYSECS 60

#define	SYSLOG_MSG(message) \
	(syslog((((errno == ENETUNREACH) || (errno == EHOSTUNREACH) || \
		(errno == ECONNREFUSED)) ? LOG_WARNING : LOG_ERR), message))

static int			rexmtval = TIMEOUT;
static int			maxtimeout = 5*TIMEOUT;
static int			securetftp;
static int			debug;
static int			disable_pnp;
static int			standalone;
static uid_t			uid_nobody = UID_NOBODY;
static uid_t			gid_nobody = GID_NOBODY;
static int			reqsock = -1;
				/* file descriptor of request socket */
static socklen_t		fromlen;
static socklen_t		fromplen;
static struct sockaddr_storage	client;
static struct sockaddr_in6 	*sin6_ptr;
static struct sockaddr_in	*sin_ptr;
static struct sockaddr_in6	*from6_ptr;
static struct sockaddr_in	*from_ptr;
static int			addrfmly;
static int			peer;
static off_t			tsize;
static tftpbuf			ackbuf;
static struct sockaddr_storage	from;
static boolean_t		tsize_set;
static pid_t			child;
				/* pid of child handling delayed replys */
static int			delay_fd [2];
				/* pipe for communicating with child */
static FILE			*file;
static char			*filename;

static union {
	struct tftphdr	hdr;
	char		data[SEGSIZE + 4];
} buf;

static union {
	struct tftphdr	hdr;
	char		data[SEGSIZE];
} oackbuf;

struct	delay_info {
	long	timestamp;		/* time request received */
	int	ecode;			/* error code to return */
	struct	sockaddr_storage from;	/* address of client */
};

int	blocksize = SEGSIZE;	/* Number of data bytes in a DATA packet */

/*
 * Default directory for unqualified names
 * Used by TFTP boot procedures
 */
static char	*homedir = "/tftpboot";

struct formats {
	char	*f_mode;
	int	(*f_validate)(int);
	void	(*f_send)(struct formats *, int);
	void	(*f_recv)(struct formats *, int);
	int	f_convert;
};

static void	delayed_responder(void);
static void	tftp(struct tftphdr *, int);
static int	validate_filename(int);
static void	tftpd_sendfile(struct formats *, int);
static void	tftpd_recvfile(struct formats *, int);
static void	nak(int);
static char	*blksize_handler(int, char *, int *);
static char	*timeout_handler(int, char *, int *);
static char	*tsize_handler(int, char *, int *);

static struct formats formats[] = {
	{ "netascii",	validate_filename, tftpd_sendfile, tftpd_recvfile, 1 },
	{ "octet",	validate_filename, tftpd_sendfile, tftpd_recvfile, 0 },
	{ NULL }
};

struct options {
	char	*opt_name;
	char	*(*opt_handler)(int, char *, int *);
};

static struct options options[] = {
	{ "blksize",	blksize_handler },
	{ "timeout",	timeout_handler },
	{ "tsize",	tsize_handler },
	{ NULL }
};

static char		optbuf[MAX_OPTVAL_LEN];
static int		timeout;
static sigjmp_buf	timeoutbuf;

int
main(int argc, char **argv)
{
	struct tftphdr *tp;
	int n;
	int c;
	struct	passwd *pwd;		/* for "nobody" entry */
	struct in_addr ipv4addr;
	char abuf[INET6_ADDRSTRLEN];
	socklen_t addrlen;

	openlog("tftpd", LOG_PID, LOG_DAEMON);

	pwd = getpwnam("nobody");
	if (pwd != NULL) {
		uid_nobody = pwd->pw_uid;
		gid_nobody = pwd->pw_gid;
	}

	(void) __init_daemon_priv(
	    PU_LIMITPRIVS,
	    uid_nobody, gid_nobody,
	    PRIV_PROC_FORK, PRIV_PROC_CHROOT, PRIV_NET_PRIVADDR, NULL);

	/*
	 *  Limit set is still "all."  Trim it down to just what we need:
	 *  fork and chroot.
	 */
	(void) priv_set(PRIV_SET, PRIV_ALLSETS,
	    PRIV_PROC_FORK, PRIV_PROC_CHROOT, PRIV_NET_PRIVADDR, NULL);
	(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
	(void) priv_set(PRIV_SET, PRIV_INHERITABLE, NULL);

	while ((c = getopt(argc, argv, "dspST:")) != EOF)
		switch (c) {
		case 'd':		/* enable debug */
			debug++;
			continue;
		case 's':		/* secure daemon */
			securetftp = 1;
			continue;
		case 'p':		/* disable name pnp mapping */
			disable_pnp = 1;
			continue;
		case 'S':
			standalone = 1;
			continue;
		case 'T':
			rexmtval = atoi(optarg);
			if (rexmtval <= 0 || rexmtval > MAX_TIMEOUT) {
				(void) fprintf(stderr,
				    "%s: Invalid retransmission "
				    "timeout value: %s\n", argv[0], optarg);
				exit(1);
			}
			maxtimeout = 5 * rexmtval;
			continue;
		case '?':
		default:
usage:
			(void) fprintf(stderr,
			    "usage: %s [-T rexmtval] [-spd] [home-directory]\n",
			    argv[0]);
			for (; optind < argc; optind++)
				syslog(LOG_ERR, "bad argument %s",
				    argv[optind]);
			exit(1);
		}

	if (optind < argc)
		if (optind == argc - 1 && *argv [optind] == '/')
			homedir = argv [optind];
		else
			goto usage;

	if (pipe(delay_fd) < 0) {
		syslog(LOG_ERR, "pipe (main): %m");
		exit(1);
	}

	(void) sigset(SIGCHLD, SIG_IGN); /* no zombies please */

	if (standalone) {
		socklen_t clientlen;

		sin6_ptr = (struct sockaddr_in6 *)&client;
		clientlen = sizeof (struct sockaddr_in6);
		reqsock = socket(AF_INET6, SOCK_DGRAM, 0);
		if (reqsock == -1) {
			perror("socket");
			exit(1);
		}
		(void) memset(&client, 0, clientlen);
		sin6_ptr->sin6_family = AF_INET6;
		sin6_ptr->sin6_port = htons(IPPORT_TFTP);

		/* Enable privilege as tftp port is < 1024 */
		(void) priv_set(PRIV_SET,
		    PRIV_EFFECTIVE, PRIV_NET_PRIVADDR, NULL);
		if (bind(reqsock, (struct sockaddr *)&client,
		    clientlen) == -1) {
			perror("bind");
			exit(1);
		}
		(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);

		if (debug)
			(void) puts("running in standalone mode...");
	} else {
		/* request socket passed on fd 0 by inetd */
		reqsock = 0;
	}
	if (debug) {
		int on = 1;

		(void) setsockopt(reqsock, SOL_SOCKET, SO_DEBUG,
		    (char *)&on, sizeof (on));
	}

	(void) chdir(homedir);

	(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, PRIV_PROC_FORK, NULL);
	if ((child = fork()) < 0) {
		syslog(LOG_ERR, "fork (main): %m");
		exit(1);
	}
	(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);

	if (child == 0) {
		(void) priv_set(PRIV_SET, PRIV_ALLSETS, NULL);
		delayed_responder();
	} /* child */

	/* close read side of pipe */
	(void) close(delay_fd[0]);


	/*
	 * Top level handling of incomming tftp requests.  Read a request
	 * and pass it off to be handled.  If request is valid, handling
	 * forks off and parent returns to this loop.  If no new requests
	 * are received for DALLYSECS, exit and return to inetd.
	 */

	for (;;) {
		fd_set readfds;
		struct timeval dally;

		FD_ZERO(&readfds);
		FD_SET(reqsock, &readfds);
		dally.tv_sec = DALLYSECS;
		dally.tv_usec = 0;

		n = select(reqsock + 1, &readfds, NULL, NULL, &dally);
		if (n < 0) {
			if (errno == EINTR)
				continue;
			syslog(LOG_ERR, "select: %m");
			(void) kill(child, SIGKILL);
			exit(1);
		}
		if (n == 0) {
			/* Select timed out.  Its time to die. */
			if (standalone)
				continue;
			else {
				(void) kill(child, SIGKILL);
				exit(0);
			}
		}
		addrlen = sizeof (from);
		if (getsockname(reqsock, (struct sockaddr  *)&from,
		    &addrlen) < 0) {
			syslog(LOG_ERR, "getsockname: %m");
			exit(1);
		}

		switch (from.ss_family) {
		case AF_INET:
			fromlen = (socklen_t)sizeof (struct sockaddr_in);
			break;
		case AF_INET6:
			fromlen = (socklen_t)sizeof (struct sockaddr_in6);
			break;
		default:
			syslog(LOG_ERR,
			    "Unknown address Family on peer connection %d",
			    from.ss_family);
			exit(1);
		}

		n = recvfrom(reqsock, &buf, sizeof (buf), 0,
		    (struct sockaddr *)&from, &fromlen);
		if (n < 0) {
			if (errno == EINTR)
				continue;
			if (standalone)
				perror("recvfrom");
			else
				syslog(LOG_ERR, "recvfrom: %m");
			(void) kill(child, SIGKILL);
			exit(1);
		}

		(void) alarm(0);

		switch (from.ss_family) {
		case AF_INET:
			addrfmly = AF_INET;
			fromplen = sizeof (struct sockaddr_in);
			sin_ptr = (struct sockaddr_in *)&client;
			(void) memset(&client, 0, fromplen);
			sin_ptr->sin_family = AF_INET;
			break;
		case AF_INET6:
			addrfmly = AF_INET6;
			fromplen = sizeof (struct sockaddr_in6);
			sin6_ptr = (struct sockaddr_in6 *)&client;
			(void) memset(&client, 0, fromplen);
			sin6_ptr->sin6_family = AF_INET6;
			break;
		default:
			syslog(LOG_ERR,
			    "Unknown address Family on peer connection");
			exit(1);
		}
		peer = socket(addrfmly, SOCK_DGRAM, 0);
		if (peer < 0) {
			if (standalone)
				perror("socket (main)");
			else
				syslog(LOG_ERR, "socket (main): %m");
			(void) kill(child, SIGKILL);
			exit(1);
		}
		if (debug) {
			int on = 1;

			(void) setsockopt(peer, SOL_SOCKET, SO_DEBUG,
			    (char *)&on, sizeof (on));
		}

		if (bind(peer, (struct sockaddr *)&client, fromplen) < 0) {
			if (standalone)
				perror("bind (main)");
			else
				syslog(LOG_ERR, "bind (main): %m");
			(void) kill(child, SIGKILL);
			exit(1);
		}
		if (standalone && debug) {
			sin6_ptr = (struct sockaddr_in6 *)&client;
			from6_ptr = (struct sockaddr_in6 *)&from;
			if (IN6_IS_ADDR_V4MAPPED(&from6_ptr->sin6_addr)) {
				IN6_V4MAPPED_TO_INADDR(&from6_ptr->sin6_addr,
				    &ipv4addr);
				(void) inet_ntop(AF_INET, &ipv4addr, abuf,
				    sizeof (abuf));
			} else {
				(void) inet_ntop(AF_INET6,
				    &from6_ptr->sin6_addr, abuf,
				    sizeof (abuf));
			}
			/* get local port */
			if (getsockname(peer, (struct sockaddr *)&client,
			    &fromplen) < 0)
				perror("getsockname (main)");
			(void) fprintf(stderr,
			    "request from %s port %d; local port %d\n",
			    abuf, from6_ptr->sin6_port, sin6_ptr->sin6_port);
		}
		tp = &buf.hdr;
		tp->th_opcode = ntohs((ushort_t)tp->th_opcode);
		if (tp->th_opcode == RRQ || tp->th_opcode == WRQ)
			tftp(tp, n);

		(void) close(peer);
		(void) fclose(file);
	}

	/*NOTREACHED*/
	return (0);
}

static void
delayed_responder(void)
{
	struct delay_info dinfo;
	long now;

	/* we don't use the descriptors passed in to the parent */
	(void) close(0);
	(void) close(1);
	if (standalone)
		(void) close(reqsock);

	/* close write side of pipe */
	(void) close(delay_fd[1]);

	for (;;) {
		int n;

		if ((n = read(delay_fd[0], &dinfo,
		    sizeof (dinfo))) != sizeof (dinfo)) {
			if (n < 0) {
				if (errno == EINTR)
					continue;
				if (standalone)
					perror("read from pipe "
					    "(delayed responder)");
				else
					syslog(LOG_ERR, "read from pipe: %m");
			}
			exit(1);
		}
		switch (dinfo.from.ss_family) {
		case AF_INET:
			addrfmly = AF_INET;
			fromplen = sizeof (struct sockaddr_in);
			sin_ptr = (struct sockaddr_in *)&client;
			(void) memset(&client, 0, fromplen);
			sin_ptr->sin_family = AF_INET;
			break;
		case AF_INET6:
			addrfmly = AF_INET6;
			fromplen = sizeof (struct sockaddr_in6);
			sin6_ptr = (struct sockaddr_in6 *)&client;
			(void) memset(&client, 0, fromplen);
			sin6_ptr->sin6_family = AF_INET6;
			break;
		}
		peer = socket(addrfmly, SOCK_DGRAM, 0);
		if (peer == -1) {
			if (standalone)
				perror("socket (delayed responder)");
			else
				syslog(LOG_ERR, "socket (delay): %m");
			exit(1);
		}
		if (debug) {
			int on = 1;

			(void) setsockopt(peer, SOL_SOCKET, SO_DEBUG,
			    (char *)&on, sizeof (on));
		}

		if (bind(peer, (struct sockaddr *)&client, fromplen) < 0) {
			if (standalone)
				perror("bind (delayed responder)");
			else
				syslog(LOG_ERR, "bind (delay): %m");
			exit(1);
		}
		if (client.ss_family == AF_INET) {
			from_ptr = (struct sockaddr_in *)&dinfo.from;
			from_ptr->sin_family = AF_INET;
		} else {
			from6_ptr = (struct sockaddr_in6 *)&dinfo.from;
			from6_ptr->sin6_family = AF_INET6;
		}
		/*
		 * Since a request hasn't been received from the client
		 * before the delayed responder process is forked, the
		 * from variable is uninitialized.  So set it to contain
		 * the client address.
		 */
		from = dinfo.from;

		/*
		 * only sleep if DELAY_SECS has not elapsed since
		 * original request was received.  Ensure that `now'
		 * is not earlier than `dinfo.timestamp'
		 */
		now = time(0);
		if ((uint_t)(now - dinfo.timestamp) < DELAY_SECS)
			(void) sleep(DELAY_SECS - (now - dinfo.timestamp));
		nak(dinfo.ecode);
		(void) close(peer);
	} /* for */

	/* NOTREACHED */
}

/*
 * Handle the Blocksize option.
 * Return the blksize option value string to include in the OACK reply.
 */
/*ARGSUSED*/
static char *
blksize_handler(int opcode, char *optval, int *errcode)
{
	char *endp;
	int value;

	*errcode = -1;
	errno = 0;
	value = (int)strtol(optval, &endp, 10);
	if (errno != 0 || value < MIN_BLKSIZE || *endp != '\0')
		return (NULL);
	/*
	 * As the blksize value in the OACK reply can be less than the value
	 * requested, to support broken clients if the value requested is larger
	 * than allowed in the RFC, reply with the maximum value permitted.
	 */
	if (value > MAX_BLKSIZE)
		value = MAX_BLKSIZE;

	blocksize = value;
	(void) snprintf(optbuf, sizeof (optbuf), "%d", blocksize);
	return (optbuf);
}

/*
 * Handle the Timeout Interval option.
 * Return the timeout option value string to include in the OACK reply.
 */
/*ARGSUSED*/
static char *
timeout_handler(int opcode, char *optval, int *errcode)
{
	char *endp;
	int value;

	*errcode = -1;
	errno = 0;
	value = (int)strtol(optval, &endp, 10);
	if (errno != 0 || *endp != '\0')
		return (NULL);
	/*
	 * The timeout value in the OACK reply must match the value specified
	 * by the client, so if an invalid timeout is requested don't include
	 * the timeout option in the OACK reply.
	 */
	if (value < MIN_TIMEOUT || value > MAX_TIMEOUT)
		return (NULL);

	rexmtval = value;
	maxtimeout = 5 * rexmtval;
	(void) snprintf(optbuf, sizeof (optbuf), "%d", rexmtval);
	return (optbuf);
}

/*
 * Handle the Transfer Size option.
 * Return the tsize option value string to include in the OACK reply.
 */
static char *
tsize_handler(int opcode, char *optval, int *errcode)
{
	char *endp;
	longlong_t value;

	*errcode = -1;
	errno = 0;
	value = strtoll(optval, &endp, 10);
	if (errno != 0 || value < 0 || *endp != '\0')
		return (NULL);

	if (opcode == RRQ) {
		if (tsize_set == B_FALSE)
			return (NULL);
		/*
		 * The tsize value should be 0 for a read request, but to
		 * support broken clients we don't check that it is.
		 */
	} else {
#if _FILE_OFFSET_BITS == 32
		if (value > MAXOFF_T) {
			*errcode = ENOSPACE;
			return (NULL);
		}
#endif
		tsize = value;
		tsize_set = B_TRUE;
	}
	(void) snprintf(optbuf, sizeof (optbuf), OFF_T_FMT, tsize);
	return (optbuf);
}

/*
 * Process any options included by the client in the request packet.
 * Return the size of the OACK reply packet built or 0 for no OACK reply.
 */
static int
process_options(int opcode, char *opts, char *endopts)
{
	char *cp, *optname, *optval, *ostr, *oackend;
	struct tftphdr *oackp;
	int i, errcode;

	/*
	 * To continue to interoperate with broken TFTP clients, ignore
	 * null padding appended to requests which don't include options.
	 */
	cp = opts;
	while ((cp < endopts) && (*cp == '\0'))
		cp++;
	if (cp == endopts)
		return (0);

	/*
	 * Construct an Option ACKnowledgement packet if any requested option
	 * is recognized.
	 */
	oackp = &oackbuf.hdr;
	oackend = oackbuf.data + sizeof (oackbuf.data);
	oackp->th_opcode = htons((ushort_t)OACK);
	cp = (char *)&oackp->th_stuff;
	while (opts < endopts) {
		optname = opts;
		if ((optval = next_field(optname, endopts)) == NULL) {
			nak(EOPTNEG);
			exit(1);
		}
		if ((opts = next_field(optval, endopts)) == NULL) {
			nak(EOPTNEG);
			exit(1);
		}
		for (i = 0; options[i].opt_name != NULL; i++) {
			if (strcasecmp(optname, options[i].opt_name) == 0)
				break;
		}
		if (options[i].opt_name != NULL) {
			ostr = options[i].opt_handler(opcode, optval, &errcode);
			if (ostr != NULL) {
				cp += strlcpy(cp, options[i].opt_name,
				    oackend - cp) + 1;
				if (cp <= oackend)
					cp += strlcpy(cp, ostr, oackend - cp)
					    + 1;

				if (cp > oackend) {
					nak(EOPTNEG);
					exit(1);
				}
			} else if (errcode >= 0) {
				nak(errcode);
				exit(1);
			}
		}
	}
	if (cp != (char *)&oackp->th_stuff)
		return (cp - oackbuf.data);
	return (0);
}

/*
 * Handle access errors caused by client requests.
 */

static void
delay_exit(int ecode)
{
	struct delay_info dinfo;

	/*
	 * The most likely cause of an error here is that
	 * someone has broadcast an RRQ packet because s/he's
	 * trying to boot and doesn't know who the server is.
	 * Rather then sending an ERROR packet immediately, we
	 * wait a while so that the real server has a better chance
	 * of getting through (in case client has lousy Ethernet
	 * interface).  We write to a child that handles delayed
	 * ERROR packets to avoid delaying service to new
	 * requests.  Of course, we would rather just not answer
	 * RRQ packets that are broadcasted, but there's no way
	 * for a user process to determine this.
	 */

	dinfo.timestamp = time(0);

	/*
	 * If running in secure mode, we map all errors to EACCESS
	 * so that the client gets no information about which files
	 * or directories exist.
	 */
	if (securetftp)
		dinfo.ecode = EACCESS;
	else
		dinfo.ecode = ecode;

	dinfo.from = from;
	if (write(delay_fd[1], &dinfo, sizeof (dinfo)) !=
	    sizeof (dinfo)) {
		syslog(LOG_ERR, "delayed write failed.");
		(void) kill(child, SIGKILL);
		exit(1);
	}
	exit(0);
}

/*
 * Handle initial connection protocol.
 */
static void
tftp(struct tftphdr *tp, int size)
{
	char *cp;
	int readmode, ecode;
	struct formats *pf;
	char *mode;
	int fd;
	static boolean_t firsttime = B_TRUE;
	int oacklen;
	struct stat statb;

	readmode = (tp->th_opcode == RRQ);
	filename = (char *)&tp->th_stuff;
	mode = next_field(filename, &buf.data[size]);
	cp = (mode != NULL) ? next_field(mode, &buf.data[size]) : NULL;
	if (cp == NULL) {
		nak(EBADOP);
		exit(1);
	}
	if (debug && standalone) {
		(void) fprintf(stderr, "%s for %s %s ",
		    readmode ? "RRQ" : "WRQ", filename, mode);
		print_options(stderr, cp, size + buf.data - cp);
		(void) putc('\n', stderr);
	}
	for (pf = formats; pf->f_mode != NULL; pf++)
		if (strcasecmp(pf->f_mode, mode) == 0)
			break;
	if (pf->f_mode == NULL) {
		nak(EBADOP);
		exit(1);
	}

	/*
	 * XXX fork a new process to handle this request before
	 * chroot(), otherwise the parent won't be able to create a
	 * new socket as that requires library access to system files
	 * and devices.
	 */
	(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, PRIV_PROC_FORK, NULL);
	switch (fork()) {
	case -1:
		syslog(LOG_ERR, "fork (tftp): %m");
		(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
		return;
	case 0:
		(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
		break;
	default:
		(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
		return;
	}

	/*
	 * Try to see if we can access the file.  The access can still
	 * fail later if we are running in secure mode because of
	 * the chroot() call.  We only want to execute the chroot()  once.
	 */
	if (securetftp && firsttime) {
		(void) priv_set(
		    PRIV_SET, PRIV_EFFECTIVE, PRIV_PROC_CHROOT, NULL);
		if (chroot(homedir) == -1) {
			syslog(LOG_ERR,
			    "tftpd: cannot chroot to directory %s: %m\n",
			    homedir);
			delay_exit(EACCESS);
		}
		else
		{
			firsttime = B_FALSE;
		}
		(void) priv_set(PRIV_SET, PRIV_EFFECTIVE, NULL);
		(void) chdir("/");  /* cd to  new root */
	}
	(void) priv_set(PRIV_SET, PRIV_ALLSETS, NULL);

	ecode = (*pf->f_validate)(tp->th_opcode);
	if (ecode != 0)
		delay_exit(ecode);

	/* we don't use the descriptors passed in to the parent */
	(void) close(STDIN_FILENO);
	(void) close(STDOUT_FILENO);

	/*
	 * Try to open file as low-priv setuid/setgid.  Note that
	 * a chroot() has already been done.
	 */
	fd = open(filename,
	    (readmode ? O_RDONLY : (O_WRONLY|O_TRUNC)) | O_NONBLOCK);
	if ((fd < 0) || (fstat(fd, &statb) < 0))
		delay_exit((errno == ENOENT) ? ENOTFOUND : EACCESS);

	if (((statb.st_mode & ((readmode) ? S_IROTH : S_IWOTH)) == 0) ||
	    ((statb.st_mode & S_IFMT) != S_IFREG))
		delay_exit(EACCESS);

	file = fdopen(fd, readmode ? "r" : "w");
	if (file == NULL)
		delay_exit(errno + 100);

	/* Don't know the size of transfers which involve conversion */
	tsize_set = (readmode && (pf->f_convert == 0));
	if (tsize_set)
		tsize = statb.st_size;

	/* Deal with any options sent by the client */
	oacklen = process_options(tp->th_opcode, cp, buf.data + size);

	if (tp->th_opcode == WRQ)
		(*pf->f_recv)(pf, oacklen);
	else
		(*pf->f_send)(pf, oacklen);

	exit(0);
}

/*
 *	Maybe map filename into another one.
 *
 *	For PNP, we get TFTP boot requests for filenames like
 *	<Unknown Hex IP Addr>.<Architecture Name>.   We must
 *	map these to 'pnp.<Architecture Name>'.  Note that
 *	uppercase is mapped to lowercase in the architecture names.
 *
 *	For names <Hex IP Addr> there are two cases.  First,
 *	it may be a buggy prom that omits the architecture code.
 *	So first check if <Hex IP Addr>.<arch> is on the filesystem.
 *	Second, this is how most Sun3s work; assume <arch> is sun3.
 */

static char *
pnp_check(char *origname)
{
	static char buf [MAXNAMLEN + 1];
	char *arch, *s, *bufend;
	in_addr_t ipaddr;
	int len = (origname ? strlen(origname) : 0);
	DIR *dir;
	struct dirent *dp;

	if (securetftp || disable_pnp || len < 8 || len > 14)
		return (NULL);

	/*
	 * XXX see if this cable allows pnp; if not, return NULL
	 * Requires YP support for determining this!
	 */

	ipaddr = htonl(strtol(origname, &arch, 16));
	if ((arch == NULL) || (len > 8 && *arch != '.'))
		return (NULL);
	if (len == 8)
		arch = "SUN3";
	else
		arch++;

	/*
	 * Allow <Hex IP Addr>* filename request to to be
	 * satisfied by <Hex IP Addr><Any Suffix> rather
	 * than enforcing this to be Sun3 systems.  Also serves
	 * to make case of suffix a don't-care.
	 */
	if ((dir = opendir(homedir)) == NULL)
		return (NULL);
	while ((dp = readdir(dir)) != NULL) {
		if (strncmp(origname, dp->d_name, 8) == 0) {
			(void) strlcpy(buf, dp->d_name, sizeof (buf));
			(void) closedir(dir);
			return (buf);
		}
	}
	(void) closedir(dir);

	/*
	 * XXX maybe call YP master for most current data iff
	 * pnp is enabled.
	 */

	/*
	 * only do mapping PNP boot file name for machines that
	 * are not in the hosts database.
	 */
	if (gethostbyaddr((char *)&ipaddr, sizeof (ipaddr), AF_INET) != NULL)
		return (NULL);

	s = buf + strlcpy(buf, "pnp.", sizeof (buf));
	bufend = &buf[sizeof (buf) - 1];
	while ((*arch != '\0') && (s < bufend))
		*s++ = tolower (*arch++);
	*s = '\0';
	return (buf);
}


/*
 * Try to validate filename. If the filename doesn't exist try PNP mapping.
 */
static int
validate_filename(int mode)
{
	struct stat stbuf;
	char *origfile;

	if (stat(filename, &stbuf) < 0) {
		if (errno != ENOENT)
			return (EACCESS);
		if (mode == WRQ)
			return (ENOTFOUND);

		/* try to map requested filename into a pnp filename */
		origfile = filename;
		filename = pnp_check(origfile);
		if (filename == NULL)
			return (ENOTFOUND);

		if (stat(filename, &stbuf) < 0)
			return (errno == ENOENT ? ENOTFOUND : EACCESS);
		syslog(LOG_NOTICE, "%s -> %s\n", origfile, filename);
	}

	return (0);
}

/* ARGSUSED */
static void
timer(int signum)
{
	timeout += rexmtval;
	if (timeout >= maxtimeout)
		exit(1);
	siglongjmp(timeoutbuf, 1);
}

/*
 * Send the requested file.
 */
static void
tftpd_sendfile(struct formats *pf, int oacklen)
{
	struct tftphdr *dp;
	volatile ushort_t block = 1;
	int size, n, serrno;

	if (oacklen != 0) {
		(void) sigset(SIGALRM, timer);
		timeout = 0;
		(void) sigsetjmp(timeoutbuf, 1);
		if (debug && standalone) {
			(void) fputs("Sending OACK ", stderr);
			print_options(stderr, (char *)&oackbuf.hdr.th_stuff,
			    oacklen - 2);
			(void) putc('\n', stderr);
		}
		if (sendto(peer, &oackbuf, oacklen, 0,
		    (struct sockaddr *)&from, fromplen) != oacklen) {
			if (debug && standalone) {
				serrno = errno;
				perror("sendto (oack)");
				errno = serrno;
			}
			SYSLOG_MSG("sendto (oack): %m");
			goto abort;
		}
		(void) alarm(rexmtval); /* read the ack */
		for (;;) {
			(void) sigrelse(SIGALRM);
			n = recv(peer, &ackbuf, sizeof (ackbuf), 0);
			(void) sighold(SIGALRM);
			if (n < 0) {
				if (errno == EINTR)
					continue;
				serrno = errno;
				SYSLOG_MSG("recv (ack): %m");
				if (debug && standalone) {
					errno = serrno;
					perror("recv (ack)");
				}
				goto abort;
			}
			ackbuf.tb_hdr.th_opcode =
			    ntohs((ushort_t)ackbuf.tb_hdr.th_opcode);
			ackbuf.tb_hdr.th_block =
			    ntohs((ushort_t)ackbuf.tb_hdr.th_block);

			if (ackbuf.tb_hdr.th_opcode == ERROR) {
				if (debug && standalone) {
					(void) fprintf(stderr,
					    "received ERROR %d",
					    ackbuf.tb_hdr.th_code);
					if (n > 4)
						(void) fprintf(stderr,
						    " %.*s", n - 4,
						    ackbuf.tb_hdr.th_msg);
					(void) putc('\n', stderr);
				}
				goto abort;
			}

			if (ackbuf.tb_hdr.th_opcode == ACK) {
				if (debug && standalone)
					(void) fprintf(stderr,
					    "received ACK for block %d\n",
					    ackbuf.tb_hdr.th_block);
				if (ackbuf.tb_hdr.th_block == 0)
					break;
				/*
				 * Don't resend the OACK, avoids getting stuck
				 * in an OACK/ACK loop if the client keeps
				 * replying with a bad ACK. Client will either
				 * send a good ACK or timeout sending bad ones.
				 */
			}
		}
		cancel_alarm();
	}
	dp = r_init();
	do {
		(void) sigset(SIGALRM, timer);
		size = readit(file, &dp, pf->f_convert);
		if (size < 0) {
			nak(errno + 100);
			goto abort;
		}
		dp->th_opcode = htons((ushort_t)DATA);
		dp->th_block = htons((ushort_t)block);
		timeout = 0;
		(void) sigsetjmp(timeoutbuf, 1);
		if (debug && standalone)
			(void) fprintf(stderr, "Sending DATA block %d\n",
			    block);
		if (sendto(peer, dp, size + 4, 0,
		    (struct sockaddr *)&from,  fromplen) != size + 4) {
			if (debug && standalone) {
				serrno = errno;
				perror("sendto (data)");
				errno = serrno;
			}
			SYSLOG_MSG("sendto (data): %m");
			goto abort;
		}
		read_ahead(file, pf->f_convert);
		(void) alarm(rexmtval); /* read the ack */
		for (;;) {
			(void) sigrelse(SIGALRM);
			n = recv(peer, &ackbuf, sizeof (ackbuf), 0);
			(void) sighold(SIGALRM);
			if (n < 0) {
				if (errno == EINTR)
					continue;
				serrno = errno;
				SYSLOG_MSG("recv (ack): %m");
				if (debug && standalone) {
					errno = serrno;
					perror("recv (ack)");
				}
				goto abort;
			}
			ackbuf.tb_hdr.th_opcode =
			    ntohs((ushort_t)ackbuf.tb_hdr.th_opcode);
			ackbuf.tb_hdr.th_block =
			    ntohs((ushort_t)ackbuf.tb_hdr.th_block);

			if (ackbuf.tb_hdr.th_opcode == ERROR) {
				if (debug && standalone) {
					(void) fprintf(stderr,
					    "received ERROR %d",
					    ackbuf.tb_hdr.th_code);
					if (n > 4)
						(void) fprintf(stderr,
						    " %.*s", n - 4,
						    ackbuf.tb_hdr.th_msg);
					(void) putc('\n', stderr);
				}
				goto abort;
			}

			if (ackbuf.tb_hdr.th_opcode == ACK) {
				if (debug && standalone)
					(void) fprintf(stderr,
					    "received ACK for block %d\n",
					    ackbuf.tb_hdr.th_block);
				if (ackbuf.tb_hdr.th_block == block) {
					break;
				}
				/*
				 * Never resend the current DATA packet on
				 * receipt of a duplicate ACK, doing so would
				 * cause the "Sorcerer's Apprentice Syndrome".
				 */
			}
		}
		cancel_alarm();
		block++;
	} while (size == blocksize);

abort:
	cancel_alarm();
	(void) fclose(file);
}

/* ARGSUSED */
static void
justquit(int signum)
{
	exit(0);
}

/*
 * Receive a file.
 */
static void
tftpd_recvfile(struct formats *pf, int oacklen)
{
	struct tftphdr *dp;
	struct tftphdr *ap;    /* ack buffer */
	ushort_t block = 0;
	int n, size, acklen, serrno;

	dp = w_init();
	ap = &ackbuf.tb_hdr;
	do {
		(void) sigset(SIGALRM, timer);
		timeout = 0;
		if (oacklen == 0) {
			ap->th_opcode = htons((ushort_t)ACK);
			ap->th_block = htons((ushort_t)block);
			acklen = 4;
		} else {
			/* copy OACK packet to the ack buffer ready to send */
			(void) memcpy(&ackbuf, &oackbuf, oacklen);
			acklen = oacklen;
			oacklen = 0;
		}
		block++;
		(void) sigsetjmp(timeoutbuf, 1);
send_ack:
		if (debug && standalone) {
			if (ap->th_opcode == htons((ushort_t)ACK)) {
				(void) fprintf(stderr,
				    "Sending ACK for block %d\n", block - 1);
			} else {
				(void) fprintf(stderr, "Sending OACK ");
				print_options(stderr, (char *)&ap->th_stuff,
				    acklen - 2);
				(void) putc('\n', stderr);
			}
		}
		if (sendto(peer, &ackbuf, acklen, 0, (struct sockaddr *)&from,
		    fromplen) != acklen) {
			if (ap->th_opcode == htons((ushort_t)ACK)) {
				if (debug && standalone) {
					serrno = errno;
					perror("sendto (ack)");
					errno = serrno;
				}
				syslog(LOG_ERR, "sendto (ack): %m\n");
			} else {
				if (debug && standalone) {
					serrno = errno;
					perror("sendto (oack)");
					errno = serrno;
				}
				syslog(LOG_ERR, "sendto (oack): %m\n");
			}
			goto abort;
		}
		if (write_behind(file, pf->f_convert) < 0) {
			nak(errno + 100);
			goto abort;
		}
		(void) alarm(rexmtval);
		for (;;) {
			(void) sigrelse(SIGALRM);
			n = recv(peer, dp, blocksize + 4, 0);
			(void) sighold(SIGALRM);
			if (n < 0) { /* really? */
				if (errno == EINTR)
					continue;
				syslog(LOG_ERR, "recv (data): %m");
				goto abort;
			}
			dp->th_opcode = ntohs((ushort_t)dp->th_opcode);
			dp->th_block = ntohs((ushort_t)dp->th_block);
			if (dp->th_opcode == ERROR) {
				cancel_alarm();
				if (debug && standalone) {
					(void) fprintf(stderr,
					    "received ERROR %d", dp->th_code);
					if (n > 4)
						(void) fprintf(stderr,
						    " %.*s", n - 4, dp->th_msg);
					(void) putc('\n', stderr);
				}
				return;
			}
			if (dp->th_opcode == DATA) {
				if (debug && standalone)
					(void) fprintf(stderr,
					    "Received DATA block %d\n",
					    dp->th_block);
				if (dp->th_block == block) {
					break;   /* normal */
				}
				/* Re-synchronize with the other side */
				if (synchnet(peer) < 0) {
					nak(errno + 100);
					goto abort;
				}
				if (dp->th_block == (block-1))
					goto send_ack; /* rexmit */
			}
		}
		cancel_alarm();
		/*  size = write(file, dp->th_data, n - 4); */
		size = writeit(file, &dp, n - 4, pf->f_convert);
		if (size != (n - 4)) {
			nak((size < 0) ? (errno + 100) : ENOSPACE);
			goto abort;
		}
	} while (size == blocksize);
	if (write_behind(file, pf->f_convert) < 0) {
		nak(errno + 100);
		goto abort;
	}
	n = fclose(file);	/* close data file */
	file = NULL;
	if (n == EOF) {
		nak(errno + 100);
		goto abort;
	}

	ap->th_opcode = htons((ushort_t)ACK);    /* send the "final" ack */
	ap->th_block = htons((ushort_t)(block));
	if (debug && standalone)
		(void) fprintf(stderr, "Sending ACK for block %d\n", block);
	if (sendto(peer, &ackbuf, 4, 0, (struct sockaddr *)&from,
	    fromplen) == -1) {
		if (debug && standalone)
			perror("sendto (ack)");
	}
	(void) sigset(SIGALRM, justquit); /* just quit on timeout */
	(void) alarm(rexmtval);
	/* normally times out and quits */
	n = recv(peer, dp, blocksize + 4, 0);
	(void) alarm(0);
	dp->th_opcode = ntohs((ushort_t)dp->th_opcode);
	dp->th_block = ntohs((ushort_t)dp->th_block);
	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 */
		if (debug && standalone) {
			(void) fprintf(stderr, "Sending ACK for block %d\n",
			    block);
		}
		/* resend final ack */
		if (sendto(peer, &ackbuf, 4, 0, (struct sockaddr *)&from,
		    fromplen) == -1) {
			if (debug && standalone)
				perror("sendto (last ack)");
		}
	}

abort:
	cancel_alarm();
	if (file != NULL)
		(void) fclose(file);
}

/*
 * Send a nak packet (error message).
 * Error code passed in is one of the
 * standard TFTP codes, or a UNIX errno
 * offset by 100.
 * Handles connected as well as unconnected peer.
 */
static void
nak(int error)
{
	struct tftphdr *tp;
	int length;
	struct errmsg *pe;
	int ret;

	tp = &buf.hdr;
	tp->th_opcode = htons((ushort_t)ERROR);
	tp->th_code = htons((ushort_t)error);
	for (pe = errmsgs; pe->e_code >= 0; pe++)
		if (pe->e_code == error)
			break;
	if (pe->e_code < 0) {
		pe->e_msg = strerror(error - 100);
		tp->th_code = EUNDEF;   /* set 'undef' errorcode */
	}
	(void) strlcpy(tp->th_msg, (pe->e_msg != NULL) ? pe->e_msg : "UNKNOWN",
	    sizeof (buf) - sizeof (struct tftphdr));
	length = strlen(tp->th_msg);
	length += sizeof (struct tftphdr);
	if (debug && standalone)
		(void) fprintf(stderr, "Sending NAK: %s\n", tp->th_msg);

	ret = sendto(peer, &buf, length, 0, (struct sockaddr *)&from,
	    fromplen);
	if (ret == -1 && errno == EISCONN) {
		/* Try without an address */
		ret = send(peer, &buf, length, 0);
	}
	if (ret == -1) {
		if (standalone)
			perror("sendto (nak)");
		else
			syslog(LOG_ERR, "tftpd: nak: %m\n");
	} else if (ret != length) {
		if (standalone)
			perror("sendto (nak) lost data");
		else
			syslog(LOG_ERR, "tftpd: nak: %d lost\n", length - ret);
	}
}