/* $OpenBSD: tftpd.c,v 1.52 2024/09/20 02:00:46 jsg Exp $ */ /* * Copyright (c) 2012 David Gwynne * * 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 . * * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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(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); }