417 lines
11 KiB
C
417 lines
11 KiB
C
#include <minmax.h>
|
|
#include <net.h>
|
|
#include "pxe.h"
|
|
#include "url.h"
|
|
#include "tftp.h"
|
|
|
|
const uint8_t TimeoutTable[] = {
|
|
2, 2, 3, 3, 4, 5, 6, 7, 9, 10, 12, 15, 18, 21, 26, 31, 37, 44,
|
|
53, 64, 77, 92, 110, 132, 159, 191, 229, 255, 255, 255, 255, 0
|
|
};
|
|
struct tftp_packet {
|
|
uint16_t opcode;
|
|
uint16_t serial;
|
|
char data[];
|
|
};
|
|
|
|
static void tftp_error(struct inode *file, uint16_t errnum,
|
|
const char *errstr);
|
|
|
|
static void tftp_close_file(struct inode *inode)
|
|
{
|
|
struct pxe_pvt_inode *socket = PVT(inode);
|
|
if (!socket->tftp_goteof) {
|
|
tftp_error(inode, 0, "No error, file close");
|
|
}
|
|
core_udp_close(socket);
|
|
}
|
|
|
|
/**
|
|
* Send an ERROR packet. This is used to terminate a connection.
|
|
*
|
|
* @inode: Inode structure
|
|
* @errnum: Error number (network byte order)
|
|
* @errstr: Error string (included in packet)
|
|
*/
|
|
static void tftp_error(struct inode *inode, uint16_t errnum,
|
|
const char *errstr)
|
|
{
|
|
static struct {
|
|
uint16_t err_op;
|
|
uint16_t err_num;
|
|
char err_msg[64];
|
|
} __packed err_buf;
|
|
int len = min(strlen(errstr), sizeof(err_buf.err_msg)-1);
|
|
struct pxe_pvt_inode *socket = PVT(inode);
|
|
|
|
err_buf.err_op = TFTP_ERROR;
|
|
err_buf.err_num = errnum;
|
|
memcpy(err_buf.err_msg, errstr, len);
|
|
err_buf.err_msg[len] = '\0';
|
|
|
|
core_udp_send(socket, &err_buf, 4 + len + 1);
|
|
}
|
|
|
|
/**
|
|
* Send ACK packet. This is a common operation and so is worth canning.
|
|
*
|
|
* @param: inode, Inode pointer
|
|
* @param: ack_num, Packet # to ack (host byte order)
|
|
*
|
|
*/
|
|
static void ack_packet(struct inode *inode, uint16_t ack_num)
|
|
{
|
|
static uint16_t ack_packet_buf[2];
|
|
struct pxe_pvt_inode *socket = PVT(inode);
|
|
|
|
/* Packet number to ack */
|
|
ack_packet_buf[0] = TFTP_ACK;
|
|
ack_packet_buf[1] = htons(ack_num);
|
|
|
|
core_udp_send(socket, ack_packet_buf, 4);
|
|
}
|
|
|
|
/*
|
|
* Get a fresh packet if the buffer is drained, and we haven't hit
|
|
* EOF yet. The buffer should be filled immediately after draining!
|
|
*/
|
|
static void tftp_get_packet(struct inode *inode)
|
|
{
|
|
uint16_t last_pkt;
|
|
const uint8_t *timeout_ptr;
|
|
uint8_t timeout;
|
|
uint16_t buffersize;
|
|
uint16_t serial;
|
|
jiffies_t oldtime;
|
|
struct tftp_packet *pkt = NULL;
|
|
uint16_t buf_len;
|
|
struct pxe_pvt_inode *socket = PVT(inode);
|
|
uint16_t src_port;
|
|
uint32_t src_ip;
|
|
int err;
|
|
|
|
/*
|
|
* Start by ACKing the previous packet; this should cause
|
|
* the next packet to be sent.
|
|
*/
|
|
timeout_ptr = TimeoutTable;
|
|
timeout = *timeout_ptr++;
|
|
oldtime = jiffies();
|
|
|
|
ack_again:
|
|
ack_packet(inode, socket->tftp_lastpkt);
|
|
|
|
while (timeout) {
|
|
buf_len = socket->tftp_blksize + 4;
|
|
err = core_udp_recv(socket, socket->tftp_pktbuf, &buf_len,
|
|
&src_ip, &src_port);
|
|
if (err) {
|
|
jiffies_t now = jiffies();
|
|
|
|
if (now-oldtime >= timeout) {
|
|
oldtime = now;
|
|
timeout = *timeout_ptr++;
|
|
if (!timeout)
|
|
break;
|
|
goto ack_again;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (buf_len < 4) /* Bad size for a DATA packet */
|
|
continue;
|
|
|
|
pkt = (struct tftp_packet *)(socket->tftp_pktbuf);
|
|
if (pkt->opcode != TFTP_DATA) /* Not a data packet */
|
|
continue;
|
|
|
|
/* If goes here, recevie OK, break */
|
|
break;
|
|
}
|
|
|
|
/* time runs out */
|
|
if (timeout == 0)
|
|
kaboom();
|
|
|
|
last_pkt = socket->tftp_lastpkt;
|
|
last_pkt++;
|
|
serial = ntohs(pkt->serial);
|
|
if (serial != last_pkt) {
|
|
/*
|
|
* Wrong packet, ACK the packet and try again.
|
|
* This is presumably because the ACK got lost,
|
|
* so the server just resent the previous packet.
|
|
*/
|
|
#if 0
|
|
printf("Wrong packet, wanted %04x, got %04x\n", \
|
|
htons(last_pkt), htons(*(uint16_t *)(data+2)));
|
|
#endif
|
|
goto ack_again;
|
|
}
|
|
|
|
/* It's the packet we want. We're also EOF if the size < blocksize */
|
|
socket->tftp_lastpkt = last_pkt; /* Update last packet number */
|
|
buffersize = buf_len - 4; /* Skip TFTP header */
|
|
socket->tftp_dataptr = socket->tftp_pktbuf + 4;
|
|
socket->tftp_filepos += buffersize;
|
|
socket->tftp_bytesleft = buffersize;
|
|
if (buffersize < socket->tftp_blksize) {
|
|
/* it's the last block, ACK packet immediately */
|
|
ack_packet(inode, serial);
|
|
|
|
/* Make sure we know we are at end of file */
|
|
inode->size = socket->tftp_filepos;
|
|
socket->tftp_goteof = 1;
|
|
tftp_close_file(inode);
|
|
}
|
|
}
|
|
|
|
const struct pxe_conn_ops tftp_conn_ops = {
|
|
.fill_buffer = tftp_get_packet,
|
|
.close = tftp_close_file,
|
|
};
|
|
|
|
/**
|
|
* Open a TFTP connection to the server
|
|
*
|
|
* @param:inode, the inode to store our state in
|
|
* @param:ip, the ip to contact to get the file
|
|
* @param:filename, the file we wanna open
|
|
*
|
|
* @out: open_file_t structure, stores in file->open_file
|
|
* @out: the lenght of this file, stores in file->file_len
|
|
*
|
|
*/
|
|
void tftp_open(struct url_info *url, int flags, struct inode *inode,
|
|
const char **redir)
|
|
{
|
|
struct pxe_pvt_inode *socket = PVT(inode);
|
|
char *buf;
|
|
uint16_t buf_len;
|
|
char *p;
|
|
char *options;
|
|
char *data;
|
|
static const char rrq_tail[] = "octet\0""tsize\0""0\0""blksize\0""1408";
|
|
char rrq_packet_buf[2+2*FILENAME_MAX+sizeof rrq_tail];
|
|
char reply_packet_buf[PKTBUF_SIZE];
|
|
int err;
|
|
int buffersize;
|
|
int rrq_len;
|
|
const uint8_t *timeout_ptr;
|
|
jiffies_t timeout;
|
|
jiffies_t oldtime;
|
|
uint16_t opcode;
|
|
uint16_t blk_num;
|
|
uint64_t opdata;
|
|
uint16_t src_port;
|
|
uint32_t src_ip;
|
|
|
|
(void)redir; /* TFTP does not redirect */
|
|
(void)flags;
|
|
|
|
if (url->type != URL_OLD_TFTP) {
|
|
/*
|
|
* The TFTP URL specification allows the TFTP to end with a
|
|
* ;mode= which we just ignore.
|
|
*/
|
|
url_unescape(url->path, ';');
|
|
}
|
|
|
|
if (!url->port)
|
|
url->port = TFTP_PORT;
|
|
|
|
socket->ops = &tftp_conn_ops;
|
|
if (core_udp_open(socket))
|
|
return;
|
|
|
|
buf = rrq_packet_buf;
|
|
*(uint16_t *)buf = TFTP_RRQ; /* TFTP opcode */
|
|
buf += 2;
|
|
|
|
buf = stpcpy(buf, url->path);
|
|
|
|
buf++; /* Point *past* the final NULL */
|
|
memcpy(buf, rrq_tail, sizeof rrq_tail);
|
|
buf += sizeof rrq_tail;
|
|
|
|
rrq_len = buf - rrq_packet_buf;
|
|
|
|
timeout_ptr = TimeoutTable; /* Reset timeout */
|
|
sendreq:
|
|
timeout = *timeout_ptr++;
|
|
if (!timeout)
|
|
return; /* No file available... */
|
|
oldtime = jiffies();
|
|
|
|
core_udp_sendto(socket, rrq_packet_buf, rrq_len, url->ip, url->port);
|
|
|
|
/* If the WRITE call fails, we let the timeout take care of it... */
|
|
wait_pkt:
|
|
for (;;) {
|
|
buf_len = sizeof(reply_packet_buf);
|
|
|
|
err = core_udp_recv(socket, reply_packet_buf, &buf_len,
|
|
&src_ip, &src_port);
|
|
if (err) {
|
|
jiffies_t now = jiffies();
|
|
if (now - oldtime >= timeout)
|
|
goto sendreq;
|
|
} else {
|
|
/* Make sure the packet actually came from the server and
|
|
is long enough for a TFTP opcode */
|
|
dprintf("tftp_open: got packet buflen=%d\n", buf_len);
|
|
if ((src_ip == url->ip) && (buf_len >= 2))
|
|
break;
|
|
}
|
|
}
|
|
|
|
core_udp_disconnect(socket);
|
|
core_udp_connect(socket, src_ip, src_port);
|
|
|
|
/* filesize <- -1 == unknown */
|
|
inode->size = -1;
|
|
socket->tftp_blksize = TFTP_BLOCKSIZE;
|
|
buffersize = buf_len - 2; /* bytes after opcode */
|
|
|
|
/*
|
|
* Get the opcode type, and parse it
|
|
*/
|
|
opcode = *(uint16_t *)reply_packet_buf;
|
|
switch (opcode) {
|
|
case TFTP_ERROR:
|
|
inode->size = 0;
|
|
goto done; /* ERROR reply; don't try again */
|
|
|
|
case TFTP_DATA:
|
|
/*
|
|
* If the server doesn't support any options, we'll get a
|
|
* DATA reply instead of OACK. Stash the data in the file
|
|
* buffer and go with the default value for all options...
|
|
*
|
|
* We got a DATA packet, meaning no options are
|
|
* suported. Save the data away and consider the
|
|
* length undefined, *unless* this is the only
|
|
* data packet...
|
|
*/
|
|
buffersize -= 2;
|
|
if (buffersize < 0)
|
|
goto wait_pkt;
|
|
data = reply_packet_buf + 2;
|
|
blk_num = ntohs(*(uint16_t *)data);
|
|
data += 2;
|
|
if (blk_num != 1)
|
|
goto wait_pkt;
|
|
socket->tftp_lastpkt = blk_num;
|
|
if (buffersize > TFTP_BLOCKSIZE)
|
|
goto err_reply; /* Corrupt */
|
|
|
|
socket->tftp_pktbuf = malloc(TFTP_BLOCKSIZE + 4);
|
|
if (!socket->tftp_pktbuf)
|
|
goto err_reply; /* Internal error */
|
|
|
|
if (buffersize < TFTP_BLOCKSIZE) {
|
|
/*
|
|
* This is the final EOF packet, already...
|
|
* We know the filesize, but we also want to
|
|
* ack the packet and set the EOF flag.
|
|
*/
|
|
inode->size = buffersize;
|
|
socket->tftp_goteof = 1;
|
|
ack_packet(inode, blk_num);
|
|
}
|
|
|
|
socket->tftp_bytesleft = buffersize;
|
|
socket->tftp_dataptr = socket->tftp_pktbuf;
|
|
memcpy(socket->tftp_pktbuf, data, buffersize);
|
|
goto done;
|
|
|
|
case TFTP_OACK:
|
|
/*
|
|
* Now we need to parse the OACK packet to get the transfer
|
|
* and packet sizes.
|
|
*/
|
|
|
|
options = reply_packet_buf + 2;
|
|
p = options;
|
|
|
|
while (buffersize) {
|
|
const char *opt = p;
|
|
|
|
/*
|
|
* If we find an option which starts with a NUL byte,
|
|
* (a null option), we're either seeing garbage that some
|
|
* TFTP servers add to the end of the packet, or we have
|
|
* no clue how to parse the rest of the packet (what is
|
|
* an option name and what is a value?) In either case,
|
|
* discard the rest.
|
|
*/
|
|
if (!*opt)
|
|
goto done;
|
|
|
|
while (buffersize) {
|
|
if (!*p)
|
|
break; /* Found a final null */
|
|
*p++ |= 0x20;
|
|
buffersize--;
|
|
}
|
|
if (!buffersize)
|
|
break; /* Unterminated option */
|
|
|
|
/* Consume the terminal null */
|
|
p++;
|
|
buffersize--;
|
|
|
|
if (!buffersize)
|
|
break; /* No option data */
|
|
|
|
opdata = 0;
|
|
|
|
/* do convert a number-string to decimal number, just like atoi */
|
|
while (buffersize--) {
|
|
uint8_t d = *p++;
|
|
if (d == '\0')
|
|
break; /* found a final null */
|
|
d -= '0';
|
|
if (d > 9)
|
|
goto err_reply; /* Not a decimal digit */
|
|
opdata = opdata*10 + d;
|
|
}
|
|
|
|
if (!strcmp(opt, "tsize"))
|
|
inode->size = opdata;
|
|
else if (!strcmp(opt, "blksize"))
|
|
socket->tftp_blksize = opdata;
|
|
else
|
|
goto err_reply; /* Non-negotitated option returned,
|
|
no idea what it means ...*/
|
|
|
|
|
|
}
|
|
|
|
if (socket->tftp_blksize < 64 || socket->tftp_blksize > PKTBUF_SIZE)
|
|
goto err_reply;
|
|
|
|
/* Parsing successful, allocate buffer */
|
|
socket->tftp_pktbuf = malloc(socket->tftp_blksize + 4);
|
|
if (!socket->tftp_pktbuf)
|
|
goto err_reply;
|
|
else
|
|
goto done;
|
|
|
|
default:
|
|
printf("TFTP unknown opcode %d\n", ntohs(opcode));
|
|
goto err_reply;
|
|
}
|
|
|
|
err_reply:
|
|
/* Build the TFTP error packet */
|
|
tftp_error(inode, TFTP_EOPTNEG, "TFTP protocol error");
|
|
inode->size = 0;
|
|
|
|
done:
|
|
if (!inode->size)
|
|
core_udp_close(socket);
|
|
|
|
return;
|
|
}
|