File: [local] / src / usr.sbin / tftpd / tftpd.c (download)
Revision 1.50, Sun Oct 9 23:04:57 2022 UTC (19 months, 3 weeks ago) by kn
Branch: MAIN
CVS Tags: OPENBSD_7_5_BASE, OPENBSD_7_5, OPENBSD_7_4_BASE, OPENBSD_7_4, OPENBSD_7_3_BASE, OPENBSD_7_3 Changes since 1.49: +2 -2 lines
Drop fattr promise unless file creation is allowed
This is only required for the single fchmod(2) ensuring default permissions
which only happens in the -c code path.
OK millert
|
/* $OpenBSD: tftpd.c,v 1.50 2022/10/09 23:04:57 kn Exp $ */
/*
* Copyright (c) 2012 David Gwynne <dlg@uq.edu.au>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* Copyright (c) 1983 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.
*/
/*
* Trivial file transfer protocol server.
*
* This version is based on src/libexec/tftpd which includes many
* modifications by Jim Guyton <guyton@rand-unix>.
*
* It was restructured to be a persistent event driven daemon
* supporting concurrent connections by dlg for use at the University
* of Queensland in the Faculty of Engineering Architecture and
* Information Technology.
*/
#include <sys/types.h>
#include <sys/queue.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <arpa/tftp.h>
#include <netdb.h>
#include <err.h>
#include <ctype.h>
#include <errno.h>
#include <event.h>
#include <fcntl.h>
#include <paths.h>
#include <poll.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <syslog.h>
#include <unistd.h>
#include <limits.h>
#include <vis.h>
#define TIMEOUT 5 /* packet rexmt timeout */
#define TIMEOUT_MIN 1 /* minimal packet rexmt timeout */
#define TIMEOUT_MAX 255 /* maximal packet rexmt timeout */
#define RETRIES 5
#define SEEDPATH "/etc/random.seed"
struct formats;
enum opt_enum {
OPT_TSIZE = 0,
OPT_TIMEOUT,
OPT_BLKSIZE,
NOPT
};
static char *opt_names[] = {
"tsize",
"timeout",
"blksize"
};
struct opt_client {
char *o_request;
long long o_reply;
};
struct tftp_server {
struct event ev;
TAILQ_ENTRY(tftp_server) entry;
int s;
};
TAILQ_HEAD(, tftp_server) tftp_servers;
struct tftp_client {
char buf[SEGSIZE_MAX + 4];
struct event sev;
struct sockaddr_storage ss;
struct timeval tv;
TAILQ_ENTRY(tftp_client) entry;
struct opt_client *options;
size_t segment_size;
size_t packet_size;
size_t buflen;
FILE *file;
int (*fgetc)(struct tftp_client *);
int (*fputc)(struct tftp_client *, int);
u_int retries;
u_int16_t block;
int opcode;
int newline;
int sock;
};
__dead void usage(void);
const char *getip(void *);
int rdaemon(int);
void rewrite_connect(const char *);
void rewrite_events(void);
void rewrite_map(struct tftp_client *, const char *);
void rewrite_req(int, short, void *);
void rewrite_res(int, short, void *);
int tftpd_listen(const char *, const char *, int);
void tftpd_events(void);
void tftpd_recv(int, short, void *);
int retry(struct tftp_client *);
int tftp_flush(struct tftp_client *);
void tftp_end(struct tftp_client *);
void tftp(struct tftp_client *, struct tftphdr *, size_t);
void tftp_open(struct tftp_client *, const char *);
void nak(struct tftp_client *, int);
int oack(struct tftp_client *);
void oack_done(int, short, void *);
void sendfile(struct tftp_client *);
void recvfile(struct tftp_client *);
int fget_octet(struct tftp_client *);
int fput_octet(struct tftp_client *, int);
int fget_netascii(struct tftp_client *);
int fput_netascii(struct tftp_client *, int);
void file_read(struct tftp_client *);
void tftp_send(struct tftp_client *);
int tftp_wrq_ack_packet(struct tftp_client *);
void tftp_rrq_ack(int, short, void *);
void tftp_wrq_ack(struct tftp_client *client);
void tftp_wrq(int, short, void *);
void tftp_wrq_end(int, short, void *);
int parse_options(struct tftp_client *, char *, size_t,
struct opt_client *);
int validate_access(struct tftp_client *, const char *);
struct tftp_client *
client_alloc(void);
void client_free(struct tftp_client *client);
struct formats {
const char *f_mode;
int (*f_getc)(struct tftp_client *);
int (*f_putc)(struct tftp_client *, int);
} formats[] = {
{ "octet", fget_octet, fput_octet },
{ "netascii", fget_netascii, fput_netascii },
{ NULL, NULL }
};
struct errmsg {
int e_code;
const char *e_msg;
} errmsgs[] = {
{ EUNDEF, "Undefined error code" },
{ ENOTFOUND, "File not found" },
{ EACCESS, "Access violation" },
{ ENOSPACE, "Disk full or allocation exceeded" },
{ EBADOP, "Illegal TFTP operation" },
{ EBADID, "Unknown transfer ID" },
{ EEXISTS, "File already exists" },
{ ENOUSER, "No such user" },
{ EOPTNEG, "Option negotiation failed" },
{ -1, NULL }
};
struct loggers {
__dead void (*err)(int, const char *, ...)
__attribute__((__format__ (printf, 2, 3)));
__dead void (*errx)(int, const char *, ...)
__attribute__((__format__ (printf, 2, 3)));
void (*warn)(const char *, ...)
__attribute__((__format__ (printf, 1, 2)));
void (*warnx)(const char *, ...)
__attribute__((__format__ (printf, 1, 2)));
void (*info)(const char *, ...)
__attribute__((__format__ (printf, 1, 2)));
void (*debug)(const char *, ...)
__attribute__((__format__ (printf, 1, 2)));
};
const struct loggers conslogger = {
err,
errx,
warn,
warnx,
warnx, /* info */
warnx /* debug */
};
__dead void syslog_err(int, const char *, ...)
__attribute__((__format__ (printf, 2, 3)));
__dead void syslog_errx(int, const char *, ...)
__attribute__((__format__ (printf, 2, 3)));
void syslog_warn(const char *, ...)
__attribute__((__format__ (printf, 1, 2)));
void syslog_warnx(const char *, ...)
__attribute__((__format__ (printf, 1, 2)));
void syslog_info(const char *, ...)
__attribute__((__format__ (printf, 1, 2)));
void syslog_debug(const char *, ...)
__attribute__((__format__ (printf, 1, 2)));
void syslog_vstrerror(int, int, const char *, va_list)
__attribute__((__format__ (printf, 3, 0)));
const struct loggers syslogger = {
syslog_err,
syslog_errx,
syslog_warn,
syslog_warnx,
syslog_info,
syslog_debug
};
const struct loggers *logger = &conslogger;
#define lerr(_e, _f...) logger->err((_e), _f)
#define lerrx(_e, _f...) logger->errx((_e), _f)
#define lwarn(_f...) logger->warn(_f)
#define lwarnx(_f...) logger->warnx(_f)
#define linfo(_f...) logger->info(_f)
#define ldebug(_f...) logger->debug(_f)
__dead void
usage(void)
{
extern char *__progname;
fprintf(stderr, "usage: %s [-46cdivw] [-l address] [-p port] [-r socket]"
" directory\n", __progname);
exit(1);
}
int cancreate = 0;
int canwrite = 0;
int verbose = 0;
int debug = 0;
int iflag = 0;
int
main(int argc, char *argv[])
{
extern char *__progname;
int c;
struct passwd *pw;
char *dir = NULL;
char *rewrite = NULL;
char *addr = NULL;
char *port = "tftp";
int family = AF_UNSPEC;
int devnull = -1;
while ((c = getopt(argc, argv, "46cdil:p:r:vw")) != -1) {
switch (c) {
case '4':
family = AF_INET;
break;
case '6':
family = AF_INET6;
break;
case 'c':
canwrite = cancreate = 1;
break;
case 'd':
verbose = debug = 1;
break;
case 'i':
if (rewrite != NULL)
errx(1, "options -i and -r are incompatible");
iflag = 1;
break;
case 'l':
addr = optarg;
break;
case 'p':
port = optarg;
break;
case 'r':
if (iflag == 1)
errx(1, "options -i and -r are incompatible");
rewrite = optarg;
break;
case 'v':
verbose = 1;
break;
case 'w':
canwrite = 1;
break;
default:
usage();
/* NOTREACHED */
}
}
argc -= optind;
argv += optind;
if (argc != 1)
usage();
dir = argv[0];
if (geteuid() != 0)
errx(1, "need root privileges");
pw = getpwnam("_tftpd");
if (pw == NULL)
errx(1, "no _tftpd user");
if (!debug) {
openlog(__progname, LOG_PID|LOG_NDELAY, LOG_DAEMON);
tzset();
logger = &syslogger;
devnull = open(_PATH_DEVNULL, O_RDWR);
if (devnull == -1)
err(1, "open %s", _PATH_DEVNULL);
}
if (rewrite != NULL)
rewrite_connect(rewrite);
tftpd_listen(addr, port, family);
if (chroot(dir))
err(1, "chroot %s", dir);
if (chdir("/"))
err(1, "chdir %s", dir);
/* drop privs */
if (setgroups(1, &pw->pw_gid) ||
setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
errx(1, "can't drop privileges");
if (!debug && rdaemon(devnull) == -1)
err(1, "unable to daemonize");
if (cancreate) {
if (pledge("stdio rpath wpath cpath fattr dns inet", NULL) == -1)
lerr(1, "pledge");
} else if (canwrite) {
if (pledge("stdio rpath wpath dns inet", NULL) == -1)
lerr(1, "pledge");
} else {
if (pledge("stdio rpath dns inet", NULL) == -1)
lerr(1, "pledge");
}
event_init();
if (rewrite != NULL)
rewrite_events();
tftpd_events();
event_dispatch();
exit(0);
}
struct rewritemap {
struct event wrev;
struct event rdev;
struct evbuffer *wrbuf;
struct evbuffer *rdbuf;
TAILQ_HEAD(, tftp_client) clients;
int s;
};
struct rewritemap *rwmap = NULL;
void
rewrite_connect(const char *path)
{
int s;
struct sockaddr_un remote;
size_t len;
rwmap = malloc(sizeof(*rwmap));
if (rwmap == NULL)
err(1, "rewrite event malloc");
rwmap->wrbuf = evbuffer_new();
if (rwmap->wrbuf == NULL)
err(1, "rewrite wrbuf");
rwmap->rdbuf = evbuffer_new();
if (rwmap->rdbuf == NULL)
err(1, "rewrite rdbuf");
TAILQ_INIT(&rwmap->clients);
s = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
if (s == -1)
err(1, "rewrite socket");
remote.sun_family = AF_UNIX;
len = strlcpy(remote.sun_path, path, sizeof(remote.sun_path));
if (len >= sizeof(remote.sun_path))
errx(1, "rewrite socket path is too long");
len += sizeof(remote.sun_family) + 1;
if (connect(s, (struct sockaddr *)&remote, len) == -1)
err(1, "%s", path);
rwmap->s = s;
}
void
rewrite_events(void)
{
event_set(&rwmap->wrev, rwmap->s, EV_WRITE, rewrite_req, NULL);
event_set(&rwmap->rdev, rwmap->s, EV_READ | EV_PERSIST, rewrite_res, NULL);
event_add(&rwmap->rdev, NULL);
}
void
rewrite_map(struct tftp_client *client, const char *filename)
{
char *nicebuf;
if (stravis(&nicebuf, filename, VIS_SAFE|VIS_OCTAL) == -1)
lerr(1, "rwmap stravis");
if (evbuffer_add_printf(rwmap->wrbuf, "%s %s %s\n", getip(&client->ss),
client->opcode == WRQ ? "write" : "read", nicebuf) == -1)
lerr(1, "rwmap printf");
free(nicebuf);
TAILQ_INSERT_TAIL(&rwmap->clients, client, entry);
event_add(&rwmap->wrev, NULL);
}
void
rewrite_req(int fd, short events, void *arg)
{
if (evbuffer_write(rwmap->wrbuf, fd) == -1) {
switch (errno) {
case EINTR:
case EAGAIN:
event_add(&rwmap->wrev, NULL);
return;
}
lerr(1, "rewrite socket write");
}
if (EVBUFFER_LENGTH(rwmap->wrbuf))
event_add(&rwmap->wrev, NULL);
}
void
rewrite_res(int fd, short events, void *arg)
{
struct tftp_client *client;
char *filename;
size_t len;
switch (evbuffer_read(rwmap->rdbuf, fd, PATH_MAX)) {
case -1:
switch (errno) {
case EINTR:
case EAGAIN:
return;
}
lerr(1, "rewrite socket read");
case 0:
lerrx(1, "rewrite socket closed");
default:
break;
}
while ((filename = evbuffer_readln(rwmap->rdbuf, &len,
EVBUFFER_EOL_LF)) != NULL) {
client = TAILQ_FIRST(&rwmap->clients);
if (client == NULL)
lerrx(1, "unexpected rwmap reply");
TAILQ_REMOVE(&rwmap->clients, client, entry);
tftp_open(client, filename);
free(filename);
};
}
int
tftpd_listen(const char *addr, const char *port, int family)
{
struct tftp_server *server;
struct addrinfo hints, *res, *res0;
int error;
int s;
int cerrno = EADDRNOTAVAIL;
const char *cause = "getaddrinfo";
int on = 1;
memset(&hints, 0, sizeof(hints));
hints.ai_family = family;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE;
TAILQ_INIT(&tftp_servers);
error = getaddrinfo(addr, port, &hints, &res0);
if (error) {
errx(1, "%s:%s: %s", addr ? addr : "*", port,
gai_strerror(error));
}
for (res = res0; res != NULL; res = res->ai_next) {
s = socket(res->ai_family, res->ai_socktype | SOCK_NONBLOCK,
res->ai_protocol);
if (s == -1) {
cause = "socket";
cerrno = errno;
continue;
}
if (bind(s, res->ai_addr, res->ai_addrlen) == -1) {
cause = "bind";
cerrno = errno;
close(s);
continue;
}
switch (res->ai_family) {
case AF_INET:
if (setsockopt(s, IPPROTO_IP, IP_RECVDSTADDR,
&on, sizeof(on)) == -1)
err(1, "setsockopt(IP_RECVDSTADDR)");
break;
case AF_INET6:
if (setsockopt(s, IPPROTO_IPV6, IPV6_RECVPKTINFO,
&on, sizeof(on)) == -1)
err(1, "setsockopt(IPV6_RECVPKTINFO)");
break;
}
server = malloc(sizeof(*server));
if (server == NULL)
err(1, "malloc");
server->s = s;
TAILQ_INSERT_TAIL(&tftp_servers, server, entry);
}
if (TAILQ_EMPTY(&tftp_servers))
errc(1, cerrno, "%s", cause);
freeaddrinfo(res0);
return (0);
}
void
tftpd_events(void)
{
struct tftp_server *server;
TAILQ_FOREACH(server, &tftp_servers, entry) {
event_set(&server->ev, server->s, EV_READ | EV_PERSIST,
tftpd_recv, server);
event_add(&server->ev, NULL);
}
}
struct tftp_client *
client_alloc(void)
{
struct tftp_client *client;
client = calloc(1, sizeof(*client));
if (client == NULL)
return (NULL);
client->segment_size = SEGSIZE;
client->packet_size = SEGSIZE + 4;
client->tv.tv_sec = TIMEOUT;
client->tv.tv_usec = 0;
client->sock = -1;
client->file = NULL;
client->newline = 0;
return (client);
}
void
client_free(struct tftp_client *client)
{
free(client->options);
if (client->file != NULL)
fclose(client->file);
close(client->sock);
free(client);
}
void
tftpd_recv(int fd, short events, void *arg)
{
union {
struct cmsghdr hdr;
char buf[CMSG_SPACE(sizeof(struct sockaddr_storage))];
} cmsgbuf;
struct cmsghdr *cmsg;
struct msghdr msg;
struct iovec iov;
ssize_t n;
struct sockaddr_storage s_in;
int dobind = 1;
int on = 1;
struct tftphdr *tp;
struct tftp_client *client;
client = client_alloc();
if (client == NULL) {
char buf[SEGSIZE_MAX + 4];
/* no memory! flush this request... */
recv(fd, buf, SEGSIZE_MAX + 4, 0);
/* dont care if it fails */
return;
}
bzero(&msg, sizeof(msg));
iov.iov_base = client->buf;
iov.iov_len = client->packet_size;
msg.msg_name = &client->ss;
msg.msg_namelen = sizeof(client->ss);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = &cmsgbuf.buf;
msg.msg_controllen = sizeof(cmsgbuf.buf);
n = recvmsg(fd, &msg, 0);
if (n == -1) {
lwarn("recvmsg");
goto err;
}
if (n < 4)
goto err;
client->sock = socket(client->ss.ss_family,
SOCK_DGRAM | SOCK_NONBLOCK, 0);
if (client->sock == -1) {
lwarn("socket");
goto err;
}
memset(&s_in, 0, sizeof(s_in));
s_in.ss_family = client->ss.ss_family;
s_in.ss_len = client->ss.ss_len;
/* get local address if possible */
for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL;
cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == IPPROTO_IP &&
cmsg->cmsg_type == IP_RECVDSTADDR) {
memcpy(&((struct sockaddr_in *)&s_in)->sin_addr,
CMSG_DATA(cmsg), sizeof(struct in_addr));
if (((struct sockaddr_in *)&s_in)->sin_addr.s_addr ==
INADDR_BROADCAST)
dobind = 0;
break;
}
if (cmsg->cmsg_level == IPPROTO_IPV6 &&
cmsg->cmsg_type == IPV6_PKTINFO) {
struct in6_pktinfo *ipi;
ipi = (struct in6_pktinfo *)CMSG_DATA(cmsg);
memcpy(&((struct sockaddr_in6 *)&s_in)->sin6_addr,
&ipi->ipi6_addr, sizeof(struct in6_addr));
if (IN6_IS_ADDR_LINKLOCAL(&ipi->ipi6_addr))
((struct sockaddr_in6 *)&s_in)->sin6_scope_id =
ipi->ipi6_ifindex;
break;
}
}
if (dobind) {
setsockopt(client->sock, SOL_SOCKET, SO_REUSEADDR,
&on, sizeof(on));
setsockopt(client->sock, SOL_SOCKET, SO_REUSEPORT,
&on, sizeof(on));
if (bind(client->sock, (struct sockaddr *)&s_in,
s_in.ss_len) == -1) {
lwarn("bind to %s", getip(&s_in));
goto err;
}
}
if (connect(client->sock, (struct sockaddr *)&client->ss,
client->ss.ss_len) == -1) {
lwarn("connect to %s", getip(&client->ss));
goto err;
}
tp = (struct tftphdr *)client->buf;
client->opcode = ntohs(tp->th_opcode);
if (client->opcode != RRQ && client->opcode != WRQ) {
/* bad request */
goto err;
}
tftp(client, tp, n);
return;
err:
client_free(client);
}
int
parse_options(struct tftp_client *client, char *cp, size_t size,
struct opt_client *options)
{
char *option;
char *ccp;
int has_options = 0;
int i;
while (++cp < client->buf + size) {
for (i = 2, ccp = cp; i > 0; ccp++) {
if (ccp >= client->buf + size) {
/*
* Don't reject the request, just stop trying
* to parse the option and get on with it.
* Some Apple OpenFirmware versions have
* trailing garbage on the end of otherwise
* valid requests.
*/
return (has_options);
} else if (*ccp == '\0')
i--;
}
for (option = cp; *cp; cp++)
*cp = tolower((unsigned char)*cp);
for (i = 0; i < NOPT; i++) {
if (strcmp(option, opt_names[i]) == 0) {
options[i].o_request = ++cp;
has_options = 1;
}
}
cp = ccp - 1;
}
return (has_options);
}
/*
* Handle initial connection protocol.
*/
void
tftp(struct tftp_client *client, struct tftphdr *tp, size_t size)
{
struct opt_client *options;
char *cp;
int i, first = 1, ecode, to;
struct formats *pf;
char *mode = NULL;
char filename[PATH_MAX];
const char *errstr;
if (size < 5) {
ecode = EBADOP;
goto error;
}
cp = tp->th_stuff;
again:
while (cp < client->buf + size) {
if (*cp == '\0')
break;
cp++;
}
if (*cp != '\0') {
ecode = EBADOP;
goto error;
}
i = cp - tp->th_stuff;
if (i >= sizeof(filename)) {
ecode = EBADOP;
goto error;
}
memcpy(filename, tp->th_stuff, i);
filename[i] = '\0';
if (first) {
mode = ++cp;
first = 0;
goto again;
}
for (cp = mode; *cp; cp++)
*cp = tolower((unsigned char)*cp);
for (pf = formats; pf->f_mode; pf++) {
if (strcmp(pf->f_mode, mode) == 0)
break;
}
if (pf->f_mode == 0) {
ecode = EBADOP;
goto error;
}
client->fgetc = pf->f_getc;
client->fputc = pf->f_putc;
client->options = options = calloc(NOPT, sizeof(*client->options));
if (options == NULL) {
ecode = 100 + ENOMEM;
goto error;
}
if (parse_options(client, cp, size, options)) {
if (options[OPT_TIMEOUT].o_request != NULL) {
to = strtonum(options[OPT_TIMEOUT].o_request,
TIMEOUT_MIN, TIMEOUT_MAX, &errstr);
if (errstr) {
ecode = EBADOP;
goto error;
}
options[OPT_TIMEOUT].o_reply = client->tv.tv_sec = to;
}
if (options[OPT_BLKSIZE].o_request) {
client->segment_size = strtonum(
options[OPT_BLKSIZE].o_request,
SEGSIZE_MIN, SEGSIZE_MAX, &errstr);
if (errstr) {
ecode = EBADOP;
goto error;
}
client->packet_size = client->segment_size + 4;
options[OPT_BLKSIZE].o_reply = client->segment_size;
}
} else {
free(options);
client->options = NULL;
}
if (verbose) {
char nicebuf[PATH_MAX];
(void)strnvis(nicebuf, filename, PATH_MAX,
VIS_SAFE|VIS_OCTAL);
linfo("%s: %s request for '%s'", getip(&client->ss),
client->opcode == WRQ ? "write" : "read", nicebuf);
}
if (rwmap != NULL)
rewrite_map(client, filename);
else
tftp_open(client, filename);
return;
error:
nak(client, ecode);
}
void
tftp_open(struct tftp_client *client, const char *filename)
{
int ecode;
ecode = validate_access(client, filename);
if (ecode)
goto error;
if (client->options) {
if (oack(client) == -1)
goto error;
free(client->options);
client->options = NULL;
} else if (client->opcode == WRQ) {
recvfile(client);
} else
sendfile(client);
return;
error:
nak(client, ecode);
}
/*
* 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.
*/
int
validate_access(struct tftp_client *client, const char *requested)
{
int mode = client->opcode;
struct opt_client *options = client->options;
struct stat stbuf;
int fd, wmode;
const char *errstr, *filename;
char rewritten[PATH_MAX];
if (!canwrite && mode != RRQ)
return (EACCESS);
if (strcmp(requested, SEEDPATH) == 0) {
char *buf;
if (mode != RRQ)
return (EACCESS);
buf = client->buf + sizeof(client->buf) - 512;
arc4random_buf(buf, 512);
if (options != NULL && options[OPT_TSIZE].o_request)
options[OPT_TSIZE].o_reply = 512;
client->file = fmemopen(buf, 512, "r");
if (client->file == NULL)
return (errno + 100);
return (0);
}
if (iflag) {
int ret;
/*
* In -i mode, look in the directory named after the
* client address.
*/
ret = snprintf(rewritten, sizeof(rewritten), "%s/%s",
getip(&client->ss), requested);
if (ret < 0 || ret >= sizeof(rewritten))
return (ENAMETOOLONG + 100);
filename = rewritten;
} else {
retryread:
filename = requested;
}
/*
* We use a different permissions scheme if `cancreate' is
* set.
*/
wmode = O_TRUNC;
if (stat(filename, &stbuf) == -1) {
if (!cancreate) {
/*
* In -i mode, retry failed read requests from
* the root directory.
*/
if (mode == RRQ && errno == ENOENT &&
filename == rewritten)
goto retryread;
return (errno == ENOENT ? ENOTFOUND : EACCESS);
} else {
if ((errno == ENOENT) && (mode != RRQ))
wmode |= O_CREAT;
else
return (EACCESS);
}
} else {
if (mode == RRQ) {
if ((stbuf.st_mode & (S_IRUSR >> 6)) == 0)
return (EACCESS);
} else {
if ((stbuf.st_mode & (S_IWUSR >> 6)) == 0)
return (EACCESS);
}
}
if (options != NULL && options[OPT_TSIZE].o_request) {
if (mode == RRQ)
options[OPT_TSIZE].o_reply = stbuf.st_size;
else {
/* allows writes of 65535 blocks * SEGSIZE_MAX bytes */
options[OPT_TSIZE].o_reply =
strtonum(options[OPT_TSIZE].o_request,
1, 65535LL * SEGSIZE_MAX, &errstr);
if (errstr)
return (EOPTNEG);
}
}
fd = open(filename, mode == RRQ ? O_RDONLY : (O_WRONLY|wmode), 0666);
if (fd == -1)
return (errno + 100);
/*
* If the file was created, set default permissions.
*/
if ((wmode & O_CREAT) && fchmod(fd, 0666) == -1) {
int serrno = errno;
close(fd);
unlink(filename);
return (serrno + 100);
}
client->file = fdopen(fd, mode == RRQ ? "r" : "w");
if (client->file == NULL) {
close(fd);
return (errno + 100);
}
return (0);
}
int
fget_octet(struct tftp_client *client)
{
return (getc(client->file));
}
int
fput_octet(struct tftp_client *client, int c)
{
return (putc(c, client->file));
}
int
fget_netascii(struct tftp_client *client)
{
int c = -1;
switch (client->newline) {
case 0:
c = getc(client->file);
if (c == EOF)
break;
if (c == '\n' || c == '\r') {
client->newline = c;
c = '\r';
}
break;
case '\n':
client->newline = 0;
c = '\n';
break;
case '\r':
client->newline = 0;
c = '\0';
break;
}
return (c);
}
int
fput_netascii(struct tftp_client *client, int c)
{
if (client->newline == '\r') {
client->newline = 0;
if (c == '\0')
c = '\r';
} else if (c == '\r') {
client->newline = c;
return (c);
}
return (putc(c, client->file));
}
void
sendfile(struct tftp_client *client)
{
event_set(&client->sev, client->sock, EV_READ, tftp_rrq_ack, client);
client->block = 1;
file_read(client);
}
void
file_read(struct tftp_client *client)
{
u_int8_t *buf;
struct tftphdr *dp;
int i;
int c;
dp = (struct tftphdr *)client->buf;
dp->th_opcode = htons((u_short)DATA);
dp->th_block = htons(client->block);
buf = (u_int8_t *)dp->th_data;
for (i = 0; i < client->segment_size; i++) {
c = client->fgetc(client);
if (c == EOF) {
if (ferror(client->file)) {
nak(client, 100 + EIO);
return;
}
break;
}
buf[i] = c;
}
client->buflen = i + 4;
client->retries = RETRIES;
tftp_send(client);
}
void
tftp_send(struct tftp_client *client)
{
if (send(client->sock, client->buf, client->buflen, 0) == -1) {
lwarn("send(block)");
client_free(client);
return;
}
event_add(&client->sev, &client->tv);
}
void
tftp_rrq_ack(int fd, short events, void *arg)
{
struct tftp_client *client = arg;
struct tftphdr *ap; /* ack packet */
char rbuf[SEGSIZE_MIN];
ssize_t n;
if (events & EV_TIMEOUT) {
if (retry(client) == -1) {
lwarn("%s: retry", getip(&client->ss));
goto done;
}
return;
}
n = recv(fd, rbuf, sizeof(rbuf), 0);
if (n == -1) {
switch (errno) {
case EINTR:
case EAGAIN:
event_add(&client->sev, &client->tv);
return;
default:
lwarn("%s: recv", getip(&client->ss));
goto done;
}
}
ap = (struct tftphdr *)rbuf;
ap->th_opcode = ntohs((u_short)ap->th_opcode);
ap->th_block = ntohs((u_short)ap->th_block);
switch (ap->th_opcode) {
case ACK:
break;
case ERROR:
default: /* assume the worst */
goto done;
}
if (ap->th_block != client->block) {
if (tftp_flush(client) == -1) {
lwarnx("%s: flush", getip(&client->ss));
goto done;
}
if (ap->th_block != (client->block - 1))
goto done;
tftp_send(client);
return;
}
if (client->buflen != client->packet_size) {
/* this was the last packet in the stream */
goto done;
}
client->block++;
file_read(client);
return;
done:
client_free(client);
}
int
tftp_flush(struct tftp_client *client)
{
char rbuf[SEGSIZE_MIN];
ssize_t n;
for (;;) {
n = recv(client->sock, rbuf, sizeof(rbuf), 0);
if (n == -1) {
switch (errno) {
case EAGAIN:
return (0);
case EINTR:
break;
default:
return (-1);
}
}
}
}
void
recvfile(struct tftp_client *client)
{
event_set(&client->sev, client->sock, EV_READ, tftp_wrq, client);
tftp_wrq_ack(client);
}
int
tftp_wrq_ack_packet(struct tftp_client *client)
{
struct tftphdr *ap; /* ack packet */
ap = (struct tftphdr *)client->buf;
ap->th_opcode = htons((u_short)ACK);
ap->th_block = htons(client->block);
client->buflen = 4;
client->retries = RETRIES;
return (send(client->sock, client->buf, client->buflen, 0) != 4);
}
void
tftp_wrq_ack(struct tftp_client *client)
{
if (tftp_wrq_ack_packet(client) != 0) {
lwarn("tftp wrq ack");
client_free(client);
return;
}
client->block++;
event_add(&client->sev, &client->tv);
}
void
tftp_wrq(int fd, short events, void *arg)
{
char wbuf[SEGSIZE_MAX + 4];
struct tftp_client *client = arg;
struct tftphdr *dp;
ssize_t n;
int i;
if (events & EV_TIMEOUT) {
if (retry(client) == -1) {
lwarn("%s", getip(&client->ss));
goto done;
}
return;
}
n = recv(fd, wbuf, client->packet_size, 0);
if (n == -1) {
switch (errno) {
case EINTR:
case EAGAIN:
goto retry;
default:
lwarn("tftp_wrq recv");
goto done;
}
}
if (n < 4)
goto done;
dp = (struct tftphdr *)wbuf;
dp->th_opcode = ntohs((u_short)dp->th_opcode);
dp->th_block = ntohs((u_short)dp->th_block);
switch (dp->th_opcode) {
case ERROR:
goto done;
case DATA:
break;
default:
goto retry;
}
if (dp->th_block != client->block) {
if (tftp_flush(client) == -1) {
lwarnx("%s: flush", getip(&client->ss));
goto done;
}
if (dp->th_block != (client->block - 1))
goto done;
goto retry;
}
for (i = 4; i < n; i++) {
if (client->fputc(client, wbuf[i]) == EOF) {
lwarn("tftp wrq");
goto done;
}
}
if (n < client->packet_size) {
tftp_wrq_ack_packet(client);
fclose(client->file);
client->file = NULL;
event_set(&client->sev, client->sock, EV_READ,
tftp_wrq_end, client);
event_add(&client->sev, &client->tv);
return;
}
tftp_wrq_ack(client);
return;
retry:
event_add(&client->sev, &client->tv);
return;
done:
client_free(client);
}
void
tftp_wrq_end(int fd, short events, void *arg)
{
char wbuf[SEGSIZE_MAX + 4];
struct tftp_client *client = arg;
struct tftphdr *dp;
ssize_t n;
if (events & EV_TIMEOUT) {
/* this was the last packet, we can clean up */
goto done;
}
n = recv(fd, wbuf, client->packet_size, 0);
if (n == -1) {
switch (errno) {
case EINTR:
case EAGAIN:
goto retry;
default:
lwarn("tftp_wrq_end recv");
goto done;
}
}
if (n < 4)
goto done;
dp = (struct tftphdr *)wbuf;
dp->th_opcode = ntohs((u_short)dp->th_opcode);
dp->th_block = ntohs((u_short)dp->th_block);
switch (dp->th_opcode) {
case ERROR:
goto done;
case DATA:
break;
default:
goto retry;
}
if (dp->th_block != client->block)
goto done;
retry:
if (retry(client) == -1) {
lwarn("%s", getip(&client->ss));
goto done;
}
return;
done:
client_free(client);
return;
}
/*
* Send a nak packet (error message).
* Error code passed in is one of the
* standard TFTP codes, or a UNIX errno
* offset by 100.
*/
void
nak(struct tftp_client *client, int error)
{
struct tftphdr *tp;
struct errmsg *pe;
size_t length;
ssize_t rslt;
tp = (struct tftphdr *)client->buf;
tp->th_opcode = htons((u_short)ERROR);
tp->th_code = 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 = strerror(error - 100);
tp->th_code = htons(EUNDEF); /* set 'undef' errorcode */
}
length = strlcpy(tp->th_msg, pe->e_msg, client->packet_size - 5) + 5;
if (length > client->packet_size)
length = client->packet_size;
linfo("%s: nak: %s", getip(&client->ss), tp->th_msg);
rslt = send(client->sock, client->buf, length, 0);
if (rslt == -1)
lwarn("%s: nak", getip(&client->ss));
else if ((size_t)rslt != length)
lwarnx("%s: nak: sent %zd of %zu bytes", getip(&client->ss),
rslt, length);
client_free(client);
}
/*
* Send an oack packet (option acknowledgement).
*/
int
oack(struct tftp_client *client)
{
struct opt_client *options = client->options;
struct tftphdr *tp;
char *bp;
int i, n, size;
tp = (struct tftphdr *)client->buf;
bp = (char *)tp->th_stuff;
size = sizeof(client->buf) - 2;
tp->th_opcode = htons((u_short)OACK);
for (i = 0; i < NOPT; i++) {
if (options[i].o_request == NULL)
continue;
n = snprintf(bp, size, "%s%c%lld", opt_names[i], '\0',
options[i].o_reply);
if (n < 0 || n >= size) {
lwarnx("oack: no buffer space");
goto error;
}
bp += n + 1;
size -= n + 1;
if (size < 0) {
lwarnx("oack: no buffer space");
goto error;
}
}
client->buflen = bp - client->buf;
client->retries = RETRIES;
if (send(client->sock, client->buf, client->buflen, 0) == -1) {
lwarn("oack");
goto error;
}
/* no client ACK for write requests with options */
if (client->opcode == WRQ) {
client->block = 1;
event_set(&client->sev, client->sock, EV_READ,
tftp_wrq, client);
} else
event_set(&client->sev, client->sock, EV_READ,
oack_done, client);
event_add(&client->sev, &client->tv);
return (0);
error:
return (-1);
}
int
retry(struct tftp_client *client)
{
if (--client->retries == 0) {
errno = ETIMEDOUT;
return (-1);
}
tftp_send(client);
return (0);
}
void
oack_done(int fd, short events, void *arg)
{
struct tftp_client *client = arg;
struct tftphdr *ap;
ssize_t n;
if (events & EV_TIMEOUT) {
if (retry(client) == -1) {
lwarn("%s", getip(&client->ss));
goto done;
}
return;
}
n = recv(client->sock, client->buf, client->packet_size, 0);
if (n == -1) {
switch (errno) {
case EINTR:
case EAGAIN:
event_add(&client->sev, &client->tv);
return;
default:
lwarn("%s: recv", getip(&client->ss));
goto done;
}
}
if (n < 4)
goto done;
ap = (struct tftphdr *)client->buf;
ap->th_opcode = ntohs((u_short)ap->th_opcode);
ap->th_block = ntohs((u_short)ap->th_block);
if (ap->th_opcode != ACK || ap->th_block != 0)
goto done;
sendfile(client);
return;
done:
client_free(client);
}
const char *
getip(void *s)
{
struct sockaddr *sa = s;
static char hbuf[NI_MAXHOST];
if (getnameinfo(sa, sa->sa_len, hbuf, sizeof(hbuf),
NULL, 0, NI_NUMERICHOST))
strlcpy(hbuf, "0.0.0.0", sizeof(hbuf));
return(hbuf);
}
/* daemon(3) clone, intended to be used in a "r"estricted environment */
int
rdaemon(int devnull)
{
if (devnull == -1) {
errno = EBADF;
return (-1);
}
if (fcntl(devnull, F_GETFL) == -1)
return (-1);
switch (fork()) {
case -1:
return (-1);
case 0:
break;
default:
_exit(0);
}
if (setsid() == -1)
return (-1);
(void)dup2(devnull, STDIN_FILENO);
(void)dup2(devnull, STDOUT_FILENO);
(void)dup2(devnull, STDERR_FILENO);
if (devnull > 2)
(void)close(devnull);
return (0);
}
void
syslog_vstrerror(int e, int priority, const char *fmt, va_list ap)
{
char *s;
if (vasprintf(&s, fmt, ap) == -1) {
syslog(LOG_EMERG, "unable to alloc in syslog_vstrerror");
exit(1);
}
syslog(priority, "%s: %s", s, strerror(e));
free(s);
}
void
syslog_err(int ecode, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
syslog_vstrerror(errno, LOG_CRIT, fmt, ap);
va_end(ap);
exit(ecode);
}
void
syslog_errx(int ecode, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsyslog(LOG_CRIT, fmt, ap);
va_end(ap);
exit(ecode);
}
void
syslog_warn(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
syslog_vstrerror(errno, LOG_ERR, fmt, ap);
va_end(ap);
}
void
syslog_warnx(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsyslog(LOG_ERR, fmt, ap);
va_end(ap);
}
void
syslog_info(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsyslog(LOG_INFO, fmt, ap);
va_end(ap);
}
void
syslog_debug(const char *fmt, ...)
{
va_list ap;
if (!debug)
return;
va_start(ap, fmt);
vsyslog(LOG_DEBUG, fmt, ap);
va_end(ap);
}