/
niceSOFT
/
inetutils
Обзор
Документация
Войти
/
niceSOFT
/
inetutils
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/tftpd.c
887 строк
21 KB
Simon Josefsson
maint: Run 'make update-copyright'
20 янв 2026, 11:50
Не верифицирован
20 янв 2026, 11:50
1ce61cd
Код
Авторство
О чём код?
/* Copyright (C) 1995-2026 Free Software Foundation, Inc. This file is part of GNU Inetutils. GNU Inetutils is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. GNU Inetutils is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see `http://www.gnu.org/licenses/'. */ /* * Copyright (c) 1983, 1993 * The Regents of the University of California. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the University nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ /* * Trivial file transfer protocol server. * * This version includes many modifications by Jim Guyton * <guyton@rand-unix>. */ #include <config.h> #include <sys/param.h> #include <sys/ioctl.h> #ifdef HAVE_SYS_FILIO_H # include <sys/filio.h> #endif #include <sys/stat.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/tftp.h> #include <arpa/inet.h> #include <ctype.h> #include <errno.h> #include <fcntl.h> #include <netdb.h> #include <setjmp.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <syslog.h> #include <unistd.h> #include <grp.h> #include <pwd.h> #include "tftpsubs.h" #include <attribute.h> #include <xalloc.h> #include <argp.h> #include <progname.h> #include <libinetutils.h> #define TIMEOUT 5 #ifndef LOG_FTP # define LOG_FTP LOG_DAEMON /* Use generic facility. */ #endif static int peer; static int rexmtval = TIMEOUT; static int maxtimeout = 5 * TIMEOUT; static char *chrootdir = NULL; static char *group = NULL; static char *user; #ifndef DEFAULT_USER # define DEFAULT_USER "nobody" #endif /* Some systems define PKTSIZE in <arpa/tftp.h>. */ #ifndef PKTSIZE # define PKTSIZE SEGSIZE+4 #endif static char buf[PKTSIZE]; static char ackbuf[PKTSIZE]; static struct sockaddr_storage from; static socklen_t fromlen; void tftp (struct tftphdr *, int); /* * Null-terminated directory prefix list for absolute pathname requests and * search list for relative pathname requests. * * MAXDIRS should be at least as large as the number of arguments that * inetd allows (currently 20). */ #define MAXDIRS 20 static struct dirlist { char *name; int len; } dirs[MAXDIRS + 1]; static int suppress_naks; static int logging; static const char *errtomsg (int); static void nak (int); static const char *verifyhost (struct sockaddr_storage *, socklen_t); static struct argp_option options[] = { #define GRP 0 {"logging", 'l', NULL, 0, "enable logging", GRP + 1}, {"nonexistent", 'n', NULL, 0, "suppress negative acknowledgement of requests for " "nonexistent relative filenames", GRP + 1}, #undef GRP #define GRP 10 {NULL, 0, NULL, 0, "", GRP}, {"group", 'g', "GRP", 0, "set explicit group of process owner, used with '-s'", GRP + 1}, {"secure-dir", 's', "DIR", 0, "change root directory to DIR before searching and " "serving content", GRP + 1}, {"user", 'u', "USR", 0, "set name of process owner, used with '-s' and " "defaults to 'nobody'", GRP + 1}, #undef GRP {NULL, 0, NULL, 0, NULL, 0} }; static error_t parse_opt (int key, char *arg, struct argp_state *state MAYBE_UNUSED) { switch (key) { case 'l': logging = 1; break; case 'g': free (group); group = xstrdup (arg); break; case 'n': suppress_naks = 1; break; case 's': chrootdir = xstrdup (arg); break; case 'u': free (user); user = xstrdup (arg); break; default: return ARGP_ERR_UNKNOWN; } return 0; } static struct argp argp = { options, parse_opt, "directory...", "Trivial File Transfer Protocol server", NULL, NULL, NULL }; int main (int argc, char *argv[]) { int index; struct tftphdr *tp; int on, n; struct sockaddr_storage sin; user = xstrdup (DEFAULT_USER); set_program_name (argv[0]); iu_argp_init ("tftpd", default_program_authors); argp_parse (&argp, argc, argv, 0, &index, NULL); openlog ("tftpd", LOG_NDELAY | LOG_PID, LOG_FTP); if (index < argc) { struct dirlist *dirp; /* Get list of directory prefixes. Skip relative pathnames. */ for (dirp = dirs; index < argc && dirp < &dirs[MAXDIRS]; index++) { if (argv[index][0] == '/') { dirp->name = argv[index]; dirp->len = strlen (dirp->name); dirp++; } } } on = 1; if (ioctl (0, FIONBIO, &on) < 0) { syslog (LOG_ERR, "ioctl(FIONBIO): %m"); exit (EXIT_FAILURE); } fromlen = sizeof (from); n = recvfrom (0, buf, sizeof (buf), 0, (struct sockaddr *) &from, &fromlen); if (n < 0) { syslog (LOG_ERR, "recvfrom: %m\n"); exit (EXIT_FAILURE); } /* * Now that we have read the message out of the UDP * socket, we fork and exit. Thus, inetd will go back * to listening to the tftp port, and the next request * to come in will start up a new instance of tftpd. * * We do this so that inetd can run tftpd in "wait" mode. * The problem with tftpd running in "nowait" mode is that * inetd may get one or more successful "selects" on the * tftp port before we do our receive, so more than one * instance of tftpd may be started up. Worse, if tftpd * break before doing the above "recvfrom", inetd would * spawn endless instances, clogging the system. */ { int pid; int i; socklen_t j; for (i = 1; i < 20; i++) { pid = fork (); if (pid < 0) { sleep (i); /* * flush out to most recently sent request. * * This may drop some request, but those * will be resent by the clients when * they timeout. The positive effect of * this flush is to (try to) prevent more * than one tftpd being started up to service * a single request from a single client. */ j = sizeof from; i = recvfrom (0, buf, sizeof (buf), 0, (struct sockaddr *) &from, &j); if (i > 0) { n = i; fromlen = j; } } else { break; } } if (pid < 0) { syslog (LOG_ERR, "fork: %m\n"); exit (EXIT_FAILURE); } else if (pid != 0) { exit (EXIT_SUCCESS); } } alarm (0); close (0); close (1); /* The peer's address 'from' is valid at this point. * 'from.ss_family' contains the correct address * family for any callback connection, and 'fromlen' * is the length of the corresponding address structure. */ peer = socket (from.ss_family, SOCK_DGRAM, 0); if (peer < 0) { syslog (LOG_ERR, "socket: %m\n"); exit (EXIT_FAILURE); } memset (&sin, 0, sizeof (sin)); sin.ss_family = from.ss_family; #if HAVE_STRUCT_SOCKADDR_STORAGE_SS_LEN sin.ss_len = from.ss_len; #endif if (bind (peer, (struct sockaddr *) &sin, fromlen) < 0) { syslog (LOG_ERR, "bind: %m\n"); exit (EXIT_FAILURE); } if (chrootdir && *chrootdir) { struct passwd *pwd = NULL; struct group *grp = NULL; /* Ignore user and group setting for non-root invocations. */ if (!getuid ()) { pwd = getpwnam (user); if (!pwd) { syslog (LOG_ERR, "getpwnam('%s'): %m", user); nak (ENOUSER); exit (EXIT_FAILURE); } /* Group names are not portable enough to allow * for a preset value. The server inherits * group membership from owner, in other cases. */ if (group && *group) { grp = getgrnam (group); if (!grp) { syslog (LOG_ERR, "getgrnam('%s'): %m", group); nak (ENOUSER); exit (EXIT_FAILURE); } } } if (chroot (chrootdir) || chdir ("/")) { syslog (LOG_ERR, "chroot('%s'): %m", chrootdir); nak (EACCESS); exit (EXIT_FAILURE); } if (pwd) { if (grp) { if (setgid (grp->gr_gid)) { syslog (LOG_ERR, "setgid: %m"); nak (ENOUSER); exit (EXIT_FAILURE); } } else { if (setgid (pwd->pw_gid)) { syslog (LOG_ERR, "setgid: %m"); nak (ENOUSER); exit (EXIT_FAILURE); } } if (setuid (pwd->pw_uid)) { syslog (LOG_ERR, "setuid: %m"); nak (ENOUSER); exit (EXIT_FAILURE); } } } tp = (struct tftphdr *) buf; tp->th_opcode = ntohs (tp->th_opcode); if (tp->th_opcode == RRQ || tp->th_opcode == WRQ) tftp (tp, n); exit (EXIT_FAILURE); } struct formats; int validate_access (char **, int); void tftpd_sendfile (struct formats *); void recvfile (struct formats *); struct formats { char *f_mode; int (*f_validate) (char **, int); void (*f_send) (struct formats *); void (*f_recv) (struct formats *); int f_convert; } formats[] = { {"netascii", validate_access, tftpd_sendfile, recvfile, 1}, {"octet", validate_access, tftpd_sendfile, recvfile, 0}, {0, NULL, NULL, NULL, 0} }; /* * Handle initial connection protocol. */ void tftp (struct tftphdr *tp, int size) { char *cp; int first = 1, ecode; struct formats *pf; char *filename, *mode; #if HAVE_STRUCT_TFTPHDR_TH_U filename = cp = tp->th_stuff; #else filename = cp = (char *) &(tp->th_stuff); #endif again: while (cp < buf + size) { if (*cp == '\0') break; cp++; } if (*cp != '\0') { nak (EBADOP); exit (EXIT_FAILURE); } if (first) { mode = ++cp; first = 0; goto again; } for (cp = mode; *cp; cp++) if (isupper (*cp)) *cp = tolower (*cp); for (pf = formats; pf->f_mode; pf++) if (strcmp (pf->f_mode, mode) == 0) break; if (pf->f_mode == 0) { nak (EBADOP); exit (EXIT_FAILURE); } ecode = (*pf->f_validate) (&filename, tp->th_opcode); if (logging) { char *family; switch (from.ss_family) { case AF_INET: family = "IPv4"; break; case AF_INET6: /* Should mapped IPv4 addresses be reported? */ family = "IPv6"; break; default: family = "?"; } syslog (LOG_INFO, "%s (%s): %s request for %s: %s", verifyhost (&from, fromlen), family, tp->th_opcode == WRQ ? "write" : "read", filename, errtomsg (ecode)); } if (ecode) { /* * Avoid storms of naks to a RRQ broadcast for a relative * bootfile pathname from a diskless Sun. */ if (suppress_naks && *filename != '/' && ecode == ENOTFOUND) exit (EXIT_SUCCESS); nak (ecode); exit (EXIT_FAILURE); } if (tp->th_opcode == WRQ) (*pf->f_recv) (pf); else (*pf->f_send) (pf); exit (EXIT_SUCCESS); } FILE *file; /* * Validate file access. Since we * have no uid or gid, for now require * file to exist and be publicly * readable/writable. * If we were invoked with arguments * from inetd then the file must also be * in one of the given directory prefixes. * Note also, full path name must be * given as we have no login directory. */ int validate_access (char **filep, int mode) { struct stat stbuf; int fd; struct dirlist *dirp; static char *pathname = 0; char *filename = *filep; /* * Prevent tricksters from getting around the directory restrictions */ if (strstr (filename, "/../")) return (EACCESS); if (*filename == '/') { /* * Allow the request if it's in one of the approved locations. * Special case: check the null prefix ("/") by looking * for length = 1 and relying on the arg. processing that * it's a /. */ for (dirp = dirs; dirp->name != NULL; dirp++) { if (dirp->len == 1 || (!strncmp (filename, dirp->name, dirp->len) && filename[dirp->len] == '/')) break; } /* If directory list is empty, allow access to any file */ if (dirp->name == NULL && dirp != dirs) return (EACCESS); if (stat (filename, &stbuf) < 0) return (errno == ENOENT ? ENOTFOUND : EACCESS); if ((stbuf.st_mode & S_IFMT) != S_IFREG) return (ENOTFOUND); if (mode == RRQ) { if ((stbuf.st_mode & S_IROTH) == 0) return (EACCESS); } else { if ((stbuf.st_mode & S_IWOTH) == 0) return (EACCESS); } } else { int err; /* * Relative file name: search the approved locations for it. * Don't allow write requests or ones that avoid directory * restrictions. */ if (mode != RRQ || !strncmp (filename, "../", 3)) return (EACCESS); /* * If the file exists in one of the directories and isn't * readable, continue looking. However, change the error code * to give an indication that the file exists. */ err = ENOTFOUND; for (dirp = dirs; dirp->name != NULL; dirp++) { free (pathname); pathname = malloc (strlen (dirp->name) + 1 + strlen (filename) + 1); if (!pathname) return ENOMEM; sprintf (pathname, "%s/%s", dirp->name, filename); if (stat (pathname, &stbuf) == 0 && (stbuf.st_mode & S_IFMT) == S_IFREG) { if ((stbuf.st_mode & S_IROTH) != 0) { break; } err = EACCESS; } } if (dirp->name == NULL) return (err); *filep = filename = pathname; } fd = open (filename, mode == RRQ ? O_RDONLY : (O_WRONLY | O_TRUNC)); if (fd < 0) return (errno + 100); file = fdopen (fd, (mode == RRQ) ? "r" : "w"); if (file == NULL) { return errno + 100; } return (0); } int timeout; sigjmp_buf timeoutbuf; void timer (int sig MAYBE_UNUSED) { timeout += rexmtval; if (timeout >= maxtimeout) exit (EXIT_FAILURE); siglongjmp (timeoutbuf, 1); } /* * Send the requested file. */ void tftpd_sendfile (struct formats *pf) { struct tftphdr *dp, *r_init (void); struct tftphdr *ap; /* ack packet */ int size, n; volatile int block; signal (SIGALRM, timer); dp = r_init (); ap = (struct tftphdr *) ackbuf; block = 1; do { size = readit (file, &dp, pf->f_convert); if (size < 0) { nak (errno + 100); goto abort; } dp->th_opcode = htons ((unsigned short) DATA); dp->th_block = htons ((unsigned short) block); timeout = 0; sigsetjmp (timeoutbuf, SIGALRM); send_data: if (sendto (peer, (const char *) dp, size + 4, 0, (struct sockaddr *) &from, fromlen) != size + 4) { syslog (LOG_ERR, "tftpd: write: %m\n"); goto abort; } read_ahead (file, pf->f_convert); for (;;) { alarm (rexmtval); /* read the ack */ n = recv (peer, ackbuf, sizeof (ackbuf), 0); alarm (0); if (n < 0) { syslog (LOG_ERR, "tftpd: read: %m\n"); goto abort; } ap->th_opcode = ntohs ((unsigned short) ap->th_opcode); ap->th_block = ntohs ((unsigned short) ap->th_block); if (ap->th_opcode == ERROR) goto abort; if (ap->th_opcode == ACK) { if ((unsigned short) ap->th_block == (unsigned short) block) break; /* Re-synchronize with the other side */ synchnet (peer); if ((unsigned short) ap->th_block == (unsigned short) (block - 1)) goto send_data; } } block++; } while (size == SEGSIZE); abort: fclose (file); } void justquit (int sig MAYBE_UNUSED) { exit (EXIT_SUCCESS); } /* * Receive a file. */ void recvfile (struct formats *pf) { struct tftphdr *dp, *w_init (void); struct tftphdr *ap; /* ack buffer */ int n, size; volatile int block; signal (SIGALRM, timer); dp = w_init (); ap = (struct tftphdr *) ackbuf; block = 0; do { timeout = 0; ap->th_opcode = htons ((unsigned short) ACK); ap->th_block = htons ((unsigned short) block); block++; sigsetjmp (timeoutbuf, SIGALRM); send_ack: if (sendto (peer, ackbuf, 4, 0, (struct sockaddr *) &from, fromlen) != 4) { syslog (LOG_ERR, "tftpd: write: %m\n"); goto abort; } write_behind (file, pf->f_convert); for (;;) { alarm (rexmtval); n = recv (peer, (char *) dp, PKTSIZE, 0); alarm (0); if (n < 0) { /* really? */ syslog (LOG_ERR, "tftpd: read: %m\n"); goto abort; } dp->th_opcode = ntohs ((unsigned short) dp->th_opcode); dp->th_block = ntohs ((unsigned short) dp->th_block); if (dp->th_opcode == ERROR) goto abort; if (dp->th_opcode == DATA) { if (dp->th_block == block) { break; /* normal */ } /* Re-synchronize with the other side */ synchnet (peer); if (dp->th_block == (block - 1)) goto send_ack; /* rexmit */ } } /* size = write(file, dp->th_data, n - 4); */ size = writeit (file, &dp, n - 4, pf->f_convert); if (size != (n - 4)) { /* ahem */ if (size < 0) nak (errno + 100); else nak (ENOSPACE); goto abort; } } while (size == SEGSIZE); write_behind (file, pf->f_convert); fclose (file); /* close data file */ ap->th_opcode = htons ((unsigned short) ACK); /* send the "final" ack */ ap->th_block = htons ((unsigned short) (block)); sendto (peer, ackbuf, 4, 0, (struct sockaddr *) &from, fromlen); signal (SIGALRM, justquit); /* just quit on timeout */ alarm (rexmtval); n = recv (peer, buf, sizeof (buf), 0); /* normally times out and quits */ alarm (0); if (n >= 4 && /* if read some data */ dp->th_opcode == DATA && /* and got a data block */ block == dp->th_block) { /* then my last ack was lost */ sendto (peer, ackbuf, 4, 0, (struct sockaddr *) &from, fromlen); /* resend final ack */ } abort: return; } 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"}, {-1, 0} }; static const char * errtomsg (int error) { static char buf[20]; struct errmsg *pe; if (error == 0) return "success"; for (pe = errmsgs; pe->e_code >= 0; pe++) if (pe->e_code == error) return pe->e_msg; sprintf (buf, "error %d", error); return buf; } /* * Send a nak packet (error message). * Error code passed in is one of the * standard TFTP codes, or a UNIX errno * offset by 100. */ static void nak (int error) { struct tftphdr *tp; int length; struct errmsg *pe; tp = (struct tftphdr *) buf; tp->th_opcode = htons ((unsigned short) ERROR); tp->th_code = htons ((unsigned 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 = EUNDEF; /* set 'undef' errorcode */ } length = strlen (pe->e_msg); memcpy (tp->th_msg, pe->e_msg, length); tp->th_msg[length] = '\0'; length += 5; if (sendto (peer, buf, length, 0, (struct sockaddr *) &from, fromlen) != length) syslog (LOG_ERR, "nak: %m\n"); } static const char * verifyhost (struct sockaddr_storage *fromp, socklen_t frlen) { int rc; static char host[NI_MAXHOST]; rc = getnameinfo ((struct sockaddr *) fromp, frlen, host, sizeof (host), NULL, 0, 0); if (rc == 0) return host; else { syslog (LOG_ERR, "getnameinfo: %s\n", gai_strerror (rc)); return "0.0.0.0"; } }