f3133c4149
We were seeing some timeouts when getting files with the busybox tftp client. With tcpdump, we saw that the tftp client was receiving blocks and ack'ing them, but the server was failing to receive the occasional ack. When that happened, the server would send the last block over again, but the tftp client was expecting the next block. This patch allows the client to recover from this situation (it sends an ack for the repeat block but does not write it to the local file). I hope it meets your approval, please don't hesitate to send me comments for improvement. The patch is against "head" in svn, I tested it on an older version of busybox in our environment. It applied cleanly to the older version. Credit for this goes to my co-worker John McCarthy for finding it and me for fixing it (assuming it works for everyone else too). cheerio, bjb
591 lines
13 KiB
C
591 lines
13 KiB
C
/* ------------------------------------------------------------------------- */
|
|
/* tftp.c */
|
|
/* */
|
|
/* A simple tftp client for busybox. */
|
|
/* Tries to follow RFC1350. */
|
|
/* Only "octet" mode supported. */
|
|
/* Optional blocksize negotiation (RFC2347 + RFC2348) */
|
|
/* */
|
|
/* Copyright (C) 2001 Magnus Damm <damm@opensource.se> */
|
|
/* */
|
|
/* Parts of the code based on: */
|
|
/* */
|
|
/* atftp: Copyright (C) 2000 Jean-Pierre Lefebvre <helix@step.polymtl.ca> */
|
|
/* and Remi Lefebvre <remi@debian.org> */
|
|
/* */
|
|
/* utftp: Copyright (C) 1999 Uwe Ohse <uwe@ohse.de> */
|
|
/* */
|
|
/* This program 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 2 of the License, or */
|
|
/* (at your option) any later version. */
|
|
/* */
|
|
/* This program 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, write to the Free Software */
|
|
/* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */
|
|
/* */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/time.h>
|
|
#include <sys/stat.h>
|
|
#include <netdb.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
|
|
#include "busybox.h"
|
|
|
|
//#define CONFIG_FEATURE_TFTP_DEBUG
|
|
|
|
#define TFTP_BLOCKSIZE_DEFAULT 512 /* according to RFC 1350, don't change */
|
|
#define TFTP_TIMEOUT 5 /* seconds */
|
|
|
|
/* opcodes we support */
|
|
|
|
#define TFTP_RRQ 1
|
|
#define TFTP_WRQ 2
|
|
#define TFTP_DATA 3
|
|
#define TFTP_ACK 4
|
|
#define TFTP_ERROR 5
|
|
#define TFTP_OACK 6
|
|
|
|
static const char *tftp_bb_error_msg[] = {
|
|
"Undefined error",
|
|
"File not found",
|
|
"Access violation",
|
|
"Disk full or allocation error",
|
|
"Illegal TFTP operation",
|
|
"Unknown transfer ID",
|
|
"File already exists",
|
|
"No such user"
|
|
};
|
|
|
|
static const int tftp_cmd_get = 1;
|
|
static const int tftp_cmd_put = 2;
|
|
|
|
#ifdef CONFIG_FEATURE_TFTP_BLOCKSIZE
|
|
|
|
static int tftp_blocksize_check(int blocksize, int bufsize)
|
|
{
|
|
/* Check if the blocksize is valid:
|
|
* RFC2348 says between 8 and 65464,
|
|
* but our implementation makes it impossible
|
|
* to use blocksizes smaller than 22 octets.
|
|
*/
|
|
|
|
if ((bufsize && (blocksize > bufsize)) ||
|
|
(blocksize < 8) || (blocksize > 65464)) {
|
|
bb_error_msg("bad blocksize");
|
|
return 0;
|
|
}
|
|
|
|
return blocksize;
|
|
}
|
|
|
|
static char *tftp_option_get(char *buf, int len, char *option)
|
|
{
|
|
int opt_val = 0;
|
|
int opt_found = 0;
|
|
int k;
|
|
|
|
while (len > 0) {
|
|
|
|
/* Make sure the options are terminated correctly */
|
|
|
|
for (k = 0; k < len; k++) {
|
|
if (buf[k] == '\0') {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (k >= len) {
|
|
break;
|
|
}
|
|
|
|
if (opt_val == 0) {
|
|
if (strcasecmp(buf, option) == 0) {
|
|
opt_found = 1;
|
|
}
|
|
}
|
|
else {
|
|
if (opt_found) {
|
|
return buf;
|
|
}
|
|
}
|
|
|
|
k++;
|
|
|
|
buf += k;
|
|
len -= k;
|
|
|
|
opt_val ^= 1;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
#endif
|
|
|
|
static inline int tftp(const int cmd, const struct hostent *host,
|
|
const char *remotefile, int localfd, const unsigned short port, int tftp_bufsize)
|
|
{
|
|
const int cmd_get = cmd & tftp_cmd_get;
|
|
const int cmd_put = cmd & tftp_cmd_put;
|
|
const int bb_tftp_num_retries = 5;
|
|
|
|
struct sockaddr_in sa;
|
|
struct sockaddr_in from;
|
|
struct timeval tv;
|
|
socklen_t fromlen;
|
|
fd_set rfds;
|
|
char *cp;
|
|
unsigned short tmp;
|
|
int socketfd;
|
|
int len;
|
|
int opcode = 0;
|
|
int finished = 0;
|
|
int timeout = bb_tftp_num_retries;
|
|
unsigned short block_nr = 1;
|
|
|
|
#ifdef CONFIG_FEATURE_TFTP_BLOCKSIZE
|
|
int want_option_ack = 0;
|
|
#endif
|
|
|
|
/* Can't use RESERVE_CONFIG_BUFFER here since the allocation
|
|
* size varies meaning BUFFERS_GO_ON_STACK would fail */
|
|
char *buf=xmalloc(tftp_bufsize + 4);
|
|
|
|
tftp_bufsize += 4;
|
|
|
|
if ((socketfd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
|
|
bb_perror_msg("socket");
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
len = sizeof(sa);
|
|
|
|
memset(&sa, 0, len);
|
|
bind(socketfd, (struct sockaddr *)&sa, len);
|
|
|
|
sa.sin_family = host->h_addrtype;
|
|
sa.sin_port = port;
|
|
memcpy(&sa.sin_addr, (struct in_addr *) host->h_addr,
|
|
sizeof(sa.sin_addr));
|
|
|
|
/* build opcode */
|
|
|
|
if (cmd_get) {
|
|
opcode = TFTP_RRQ;
|
|
}
|
|
|
|
if (cmd_put) {
|
|
opcode = TFTP_WRQ;
|
|
}
|
|
|
|
while (1) {
|
|
|
|
cp = buf;
|
|
|
|
/* first create the opcode part */
|
|
|
|
*((unsigned short *) cp) = htons(opcode);
|
|
|
|
cp += 2;
|
|
|
|
/* add filename and mode */
|
|
|
|
if ((cmd_get && (opcode == TFTP_RRQ)) ||
|
|
(cmd_put && (opcode == TFTP_WRQ))) {
|
|
int too_long = 0;
|
|
|
|
/* see if the filename fits into buf */
|
|
/* and fill in packet */
|
|
|
|
len = strlen(remotefile) + 1;
|
|
|
|
if ((cp + len) >= &buf[tftp_bufsize - 1]) {
|
|
too_long = 1;
|
|
}
|
|
else {
|
|
safe_strncpy(cp, remotefile, len);
|
|
cp += len;
|
|
}
|
|
|
|
if (too_long || ((&buf[tftp_bufsize - 1] - cp) < 6)) {
|
|
bb_error_msg("too long remote-filename");
|
|
break;
|
|
}
|
|
|
|
/* add "mode" part of the package */
|
|
|
|
memcpy(cp, "octet", 6);
|
|
cp += 6;
|
|
|
|
#ifdef CONFIG_FEATURE_TFTP_BLOCKSIZE
|
|
|
|
len = tftp_bufsize - 4; /* data block size */
|
|
|
|
if (len != TFTP_BLOCKSIZE_DEFAULT) {
|
|
|
|
if ((&buf[tftp_bufsize - 1] - cp) < 15) {
|
|
bb_error_msg("too long remote-filename");
|
|
break;
|
|
}
|
|
|
|
/* add "blksize" + number of blocks */
|
|
|
|
memcpy(cp, "blksize", 8);
|
|
cp += 8;
|
|
|
|
cp += snprintf(cp, 6, "%d", len) + 1;
|
|
|
|
want_option_ack = 1;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/* add ack and data */
|
|
|
|
if ((cmd_get && (opcode == TFTP_ACK)) ||
|
|
(cmd_put && (opcode == TFTP_DATA))) {
|
|
|
|
*((unsigned short *) cp) = htons(block_nr);
|
|
|
|
cp += 2;
|
|
|
|
block_nr++;
|
|
|
|
if (cmd_put && (opcode == TFTP_DATA)) {
|
|
len = bb_full_read(localfd, cp, tftp_bufsize - 4);
|
|
|
|
if (len < 0) {
|
|
bb_perror_msg("read");
|
|
break;
|
|
}
|
|
|
|
if (len != (tftp_bufsize - 4)) {
|
|
finished++;
|
|
}
|
|
|
|
cp += len;
|
|
}
|
|
}
|
|
|
|
|
|
/* send packet */
|
|
|
|
|
|
timeout = bb_tftp_num_retries; /* re-initialize */
|
|
do {
|
|
|
|
len = cp - buf;
|
|
|
|
#ifdef CONFIG_FEATURE_TFTP_DEBUG
|
|
fprintf(stderr, "sending %u bytes\n", len);
|
|
for (cp = buf; cp < &buf[len]; cp++)
|
|
fprintf(stderr, "%02x ", (unsigned char)*cp);
|
|
fprintf(stderr, "\n");
|
|
#endif
|
|
if (sendto(socketfd, buf, len, 0,
|
|
(struct sockaddr *) &sa, sizeof(sa)) < 0) {
|
|
bb_perror_msg("send");
|
|
len = -1;
|
|
break;
|
|
}
|
|
|
|
|
|
if (finished && (opcode == TFTP_ACK)) {
|
|
break;
|
|
}
|
|
|
|
/* receive packet */
|
|
|
|
memset(&from, 0, sizeof(from));
|
|
fromlen = sizeof(from);
|
|
|
|
tv.tv_sec = TFTP_TIMEOUT;
|
|
tv.tv_usec = 0;
|
|
|
|
FD_ZERO(&rfds);
|
|
FD_SET(socketfd, &rfds);
|
|
|
|
switch (select(FD_SETSIZE, &rfds, NULL, NULL, &tv)) {
|
|
case 1:
|
|
len = recvfrom(socketfd, buf, tftp_bufsize, 0,
|
|
(struct sockaddr *) &from, &fromlen);
|
|
|
|
if (len < 0) {
|
|
bb_perror_msg("recvfrom");
|
|
break;
|
|
}
|
|
|
|
timeout = 0;
|
|
|
|
if (sa.sin_port == port) {
|
|
sa.sin_port = from.sin_port;
|
|
}
|
|
if (sa.sin_port == from.sin_port) {
|
|
break;
|
|
}
|
|
|
|
/* fall-through for bad packets! */
|
|
/* discard the packet - treat as timeout */
|
|
timeout = bb_tftp_num_retries;
|
|
|
|
case 0:
|
|
bb_error_msg("timeout");
|
|
|
|
timeout--;
|
|
if (timeout == 0) {
|
|
len = -1;
|
|
bb_error_msg("last timeout");
|
|
}
|
|
break;
|
|
|
|
default:
|
|
bb_perror_msg("select");
|
|
len = -1;
|
|
}
|
|
|
|
} while (timeout && (len >= 0));
|
|
|
|
if ((finished) || (len < 0)) {
|
|
break;
|
|
}
|
|
|
|
/* process received packet */
|
|
|
|
|
|
opcode = ntohs(*((unsigned short *) buf));
|
|
tmp = ntohs(*((unsigned short *) &buf[2]));
|
|
|
|
#ifdef CONFIG_FEATURE_TFTP_DEBUG
|
|
fprintf(stderr, "received %d bytes: %04x %04x\n", len, opcode, tmp);
|
|
#endif
|
|
|
|
if (opcode == TFTP_ERROR) {
|
|
char *msg = NULL;
|
|
|
|
if (buf[4] != '\0') {
|
|
msg = &buf[4];
|
|
buf[tftp_bufsize - 1] = '\0';
|
|
} else if (tmp < (sizeof(tftp_bb_error_msg)
|
|
/ sizeof(char *))) {
|
|
|
|
msg = (char *) tftp_bb_error_msg[tmp];
|
|
}
|
|
|
|
if (msg) {
|
|
bb_error_msg("server says: %s", msg);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
#ifdef CONFIG_FEATURE_TFTP_BLOCKSIZE
|
|
if (want_option_ack) {
|
|
|
|
want_option_ack = 0;
|
|
|
|
if (opcode == TFTP_OACK) {
|
|
|
|
/* server seems to support options */
|
|
|
|
char *res;
|
|
|
|
res = tftp_option_get(&buf[2], len-2,
|
|
"blksize");
|
|
|
|
if (res) {
|
|
int blksize = atoi(res);
|
|
|
|
if (tftp_blocksize_check(blksize,
|
|
tftp_bufsize - 4)) {
|
|
|
|
if (cmd_put) {
|
|
opcode = TFTP_DATA;
|
|
}
|
|
else {
|
|
opcode = TFTP_ACK;
|
|
}
|
|
#ifdef CONFIG_FEATURE_TFTP_DEBUG
|
|
fprintf(stderr, "using blksize %u\n", blksize);
|
|
#endif
|
|
tftp_bufsize = blksize + 4;
|
|
block_nr = 0;
|
|
continue;
|
|
}
|
|
}
|
|
/* FIXME:
|
|
* we should send ERROR 8 */
|
|
bb_error_msg("bad server option");
|
|
break;
|
|
}
|
|
|
|
bb_error_msg("warning: blksize not supported by server"
|
|
" - reverting to 512");
|
|
|
|
tftp_bufsize = TFTP_BLOCKSIZE_DEFAULT + 4;
|
|
}
|
|
#endif
|
|
|
|
if (cmd_get && (opcode == TFTP_DATA)) {
|
|
|
|
if (tmp == block_nr) {
|
|
|
|
len = bb_full_write(localfd, &buf[4], len - 4);
|
|
|
|
if (len < 0) {
|
|
bb_perror_msg("write");
|
|
break;
|
|
}
|
|
|
|
if (len != (tftp_bufsize - 4)) {
|
|
finished++;
|
|
}
|
|
|
|
opcode = TFTP_ACK;
|
|
continue;
|
|
}
|
|
/* in case the last ack disappeared into the ether */
|
|
if ( tmp == (block_nr - 1) ) {
|
|
--block_nr;
|
|
opcode = TFTP_ACK;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (cmd_put && (opcode == TFTP_ACK)) {
|
|
|
|
if (tmp == (unsigned short)(block_nr - 1)) {
|
|
if (finished) {
|
|
break;
|
|
}
|
|
|
|
opcode = TFTP_DATA;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
#ifdef CONFIG_FEATURE_CLEAN_UP
|
|
close(socketfd);
|
|
|
|
free(buf);
|
|
#endif
|
|
|
|
return finished ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
}
|
|
|
|
int tftp_main(int argc, char **argv)
|
|
{
|
|
struct hostent *host = NULL;
|
|
const char *localfile = NULL;
|
|
const char *remotefile = NULL;
|
|
int port;
|
|
int cmd = 0;
|
|
int fd = -1;
|
|
int flags = 0;
|
|
int opt;
|
|
int result;
|
|
int blocksize = TFTP_BLOCKSIZE_DEFAULT;
|
|
|
|
/* figure out what to pass to getopt */
|
|
|
|
#ifdef CONFIG_FEATURE_TFTP_BLOCKSIZE
|
|
#define BS "b:"
|
|
#else
|
|
#define BS
|
|
#endif
|
|
|
|
#ifdef CONFIG_FEATURE_TFTP_GET
|
|
#define GET "g"
|
|
#else
|
|
#define GET
|
|
#endif
|
|
|
|
#ifdef CONFIG_FEATURE_TFTP_PUT
|
|
#define PUT "p"
|
|
#else
|
|
#define PUT
|
|
#endif
|
|
|
|
while ((opt = getopt(argc, argv, BS GET PUT "l:r:")) != -1) {
|
|
switch (opt) {
|
|
#ifdef CONFIG_FEATURE_TFTP_BLOCKSIZE
|
|
case 'b':
|
|
blocksize = atoi(optarg);
|
|
if (!tftp_blocksize_check(blocksize, 0)) {
|
|
return EXIT_FAILURE;
|
|
}
|
|
break;
|
|
#endif
|
|
#ifdef CONFIG_FEATURE_TFTP_GET
|
|
case 'g':
|
|
cmd = tftp_cmd_get;
|
|
flags = O_WRONLY | O_CREAT | O_TRUNC;
|
|
break;
|
|
#endif
|
|
#ifdef CONFIG_FEATURE_TFTP_PUT
|
|
case 'p':
|
|
cmd = tftp_cmd_put;
|
|
flags = O_RDONLY;
|
|
break;
|
|
#endif
|
|
case 'l':
|
|
localfile = optarg;
|
|
break;
|
|
case 'r':
|
|
remotefile = optarg;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ((cmd == 0) || (optind == argc)) {
|
|
bb_show_usage();
|
|
}
|
|
if(localfile && strcmp(localfile, "-") == 0) {
|
|
fd = fileno((cmd==tftp_cmd_get)? stdout : stdin);
|
|
}
|
|
if(localfile == NULL)
|
|
localfile = remotefile;
|
|
if(remotefile == NULL)
|
|
remotefile = localfile;
|
|
if (fd==-1) {
|
|
fd = open(localfile, flags, 0644);
|
|
}
|
|
if (fd < 0) {
|
|
bb_perror_msg_and_die("local file");
|
|
}
|
|
|
|
host = xgethostbyname(argv[optind]);
|
|
port = bb_lookup_port(argv[optind + 1], "udp", 69);
|
|
|
|
#ifdef CONFIG_FEATURE_TFTP_DEBUG
|
|
fprintf(stderr, "using server \"%s\", remotefile \"%s\", "
|
|
"localfile \"%s\".\n",
|
|
inet_ntoa(*((struct in_addr *) host->h_addr)),
|
|
remotefile, localfile);
|
|
#endif
|
|
|
|
result = tftp(cmd, host, remotefile, fd, port, blocksize);
|
|
|
|
#ifdef CONFIG_FEATURE_CLEAN_UP
|
|
if (!(fd == STDOUT_FILENO || fd == STDIN_FILENO)) {
|
|
close(fd);
|
|
}
|
|
#endif
|
|
return(result);
|
|
}
|