From 3611cb17b74740588059c5fe2d11545c6e11f573 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Tue, 8 Aug 2006 23:45:52 +0000 Subject: [PATCH] Initial (untested) implementation of TFTP over the new UDP API. --- src/include/gpxe/tftp.h | 22 +- src/net/udp/tftp.c | 471 ++++++++++++++++++++++++++++++++++++++++ src/proto/tftp.c | 165 -------------- 3 files changed, 491 insertions(+), 167 deletions(-) create mode 100644 src/net/udp/tftp.c delete mode 100644 src/proto/tftp.c diff --git a/src/include/gpxe/tftp.h b/src/include/gpxe/tftp.h index f6b738e6..e5c599bc 100644 --- a/src/include/gpxe/tftp.h +++ b/src/include/gpxe/tftp.h @@ -9,6 +9,8 @@ #include #include +#include +#include #define TFTP_PORT 69 /**< Default TFTP server port */ #define TFTP_DEFAULT_BLKSIZE 512 /**< Default TFTP data block size */ @@ -59,7 +61,7 @@ struct tftp_error { /** A TFTP options acknowledgement (OACK) packet */ struct tftp_oack { uint16_t opcode; - uint8_t data[0]; + char data[0]; } __attribute__ (( packed )); /** The common header of all TFTP packets */ @@ -87,6 +89,17 @@ struct tftp_session { struct udp_connection udp; /** Filename */ const char *filename; + + /** + * Callback function + * + * @v tftp TFTP connection + * @v block Block number + * @v data Data + * @v len Length of data + */ + void ( * callback ) ( struct tftp_session *tftp, unsigned int block, + void *data, size_t len ); /** * Transfer ID * @@ -119,7 +132,12 @@ struct tftp_session { * TFTP server. If the TFTP server does not support the * "tsize" option, this value will be zero. */ - off_t tsize; + unsigned long tsize; + + /** Asynchronous operation for this session */ + struct async_operation aop; + /** Retransmission timer */ + struct retry_timer timer; }; #endif /* _GPXE_TFTP_H */ diff --git a/src/net/udp/tftp.c b/src/net/udp/tftp.c new file mode 100644 index 00000000..39839bea --- /dev/null +++ b/src/net/udp/tftp.c @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2006 Michael Brown . + * + * 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 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., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** @file + * + * TFTP protocol + * + */ + +/** A TFTP option */ +struct tftp_option { + /** Option name */ + const char *name; + /** Option processor + * + * @v tftp TFTP connection + * @v value Option value + * @ret rc Return status code + */ + int ( * process ) ( struct tftp_session *tftp, const char *value ); +}; + +/** + * Process TFTP "blksize" option + * + * @v tftp TFTP connection + * @v value Option value + * @ret rc Return status code + */ +static int tftp_process_blksize ( struct tftp_session *tftp, + const char *value ) { + char *end; + + tftp->blksize = strtoul ( value, &end, 10 ); + if ( *end ) { + DBG ( "TFTP %p got invalid blksize \"%s\"\n", tftp, value ); + return -EINVAL; + } + DBG ( "TFTP %p blksize=%d\n", tftp, tftp->blksize ); + + return 0; +} + +/** + * Process TFTP "tsize" option + * + * @v tftp TFTP connection + * @v value Option value + * @ret rc Return status code + */ +static int tftp_process_tsize ( struct tftp_session *tftp, + const char *value ) { + char *end; + + tftp->tsize = strtoul ( value, &end, 10 ); + if ( *end ) { + DBG ( "TFTP %p got invalid tsize \"%s\"\n", tftp, value ); + return -EINVAL; + } + DBG ( "TFTP %p tsize=%ld\n", tftp, tftp->tsize ); + + return 0; +} + +/** Recognised TFTP options */ +static struct tftp_option tftp_options[] = { + { "blksize", tftp_process_blksize }, + { "tsize", tftp_process_tsize }, + { NULL, NULL } +}; + +/** + * Process TFTP option + * + * @v tftp TFTP connection + * @v name Option name + * @v value Option value + * @ret rc Return status code + */ +static int tftp_process_option ( struct tftp_session *tftp, + const char *name, const char *value ) { + struct tftp_option *option; + + for ( option = tftp_options ; option->name ; option++ ) { + if ( strcasecmp ( name, option->name ) == 0 ) + return option->process ( tftp, value ); + } + + DBG ( "TFTP %p received unknown option \"%s\" = \"%s\"\n", + tftp, name, value ); + + return -EINVAL; +} + +/** Translation between TFTP errors and internal error numbers */ +static const uint8_t tftp_errors[] = { + [TFTP_ERR_FILE_NOT_FOUND] = PXENV_STATUS_TFTP_FILE_NOT_FOUND, + [TFTP_ERR_ACCESS_DENIED] = PXENV_STATUS_TFTP_ACCESS_VIOLATION, + [TFTP_ERR_ILLEGAL_OP] = PXENV_STATUS_TFTP_UNKNOWN_OPCODE, +}; + +/** + * Mark TFTP session as complete + * + * @v tftp TFTP connection + * @v rc Return status code + */ +static void tftp_done ( struct tftp_session *tftp, int rc ) { + + /* Stop the retry timer */ + stop_timer ( &tftp->timer ); + + /* Close UDP connection */ + udp_close ( &tftp->udp ); + + /* Mark async operation as complete */ + async_done ( &tftp->aop, rc ); +} + +/** + * Send next packet in TFTP session + * + * @v tftp TFTP connection + */ +static void tftp_send_packet ( struct tftp_session *tftp ) { + start_timer ( &tftp->timer ); + udp_senddata ( &tftp->udp ); +} + +/** + * Handle TFTP retransmission timer expiry + * + * @v timer Retry timer + * @v fail Failure indicator + */ +static void tftp_timer_expired ( struct retry_timer *timer, int fail ) { + struct tftp_session *tftp = + container_of ( timer, struct tftp_session, timer ); + + if ( fail ) { + tftp_done ( tftp, -ETIMEDOUT ); + } else { + tftp_send_packet ( tftp ); + } +} + +/** + * Mark TFTP block as received + * + * @v tftp TFTP connection + * @v block Block number + */ +static void tftp_received ( struct tftp_session *tftp, unsigned int block ) { + + /* Stop the retry timer */ + stop_timer ( &tftp->timer ); + + /* Update state to indicate which block we're now waiting for */ + tftp->state = block; + + /* Send next packet */ + tftp_send_packet ( tftp ); +} + +/** + * Transmit RRQ + * + * @v tftp TFTP connection + * @v buf Temporary data buffer + * @v len Length of temporary data buffer + * @ret rc Return status code + */ +static int tftp_send_rrq ( struct tftp_session *tftp, void *buf, size_t len ) { + struct tftp_rrq *rrq = buf; + void *data; + void *end; + + DBG ( "TFTP %p requesting \"%s\"\n", tftp, tftp->filename ); + + data = rrq->data; + end = ( buf + len ); + if ( data > end ) + goto overflow; + data += snprintf ( data, ( end - data ), + "%s%coctet%cblksize%c%d%ctsize%c0", + tftp->filename, 0, 0, 0, tftp->blksize, 0, 0 ) + 1; + if ( data > end ) + goto overflow; + rrq->opcode = htons ( TFTP_RRQ ); + + return udp_send ( &tftp->udp, buf, ( data - buf ) ); + + overflow: + DBG ( "TFTP %p RRQ out of space\n", tftp ); + return -ENOBUFS; +} + +/** + * Receive OACK + * + * @v tftp TFTP connection + * @v buf Temporary data buffer + * @v len Length of temporary data buffer + * @ret rc Return status code + */ +static int tftp_rx_oack ( struct tftp_session *tftp, void *buf, size_t len ) { + struct tftp_oack *oack = buf; + char *end = buf + len; + char *name; + char *value; + int rc; + + /* Sanity check */ + if ( len < sizeof ( *oack ) ) { + DBG ( "TFTP %p received underlength OACK packet length %d\n", + tftp, len ); + return -EINVAL; + } + if ( end[-1] != '\0' ) { + DBG ( "TFTP %p received OACK missing final NUL\n", tftp ); + return -EINVAL; + } + + /* Process each option in turn */ + name = oack->data; + while ( name < end ) { + value = ( name + strlen ( name ) + 1 ); + if ( value == end ) { + DBG ( "TFTP %p received OACK missing value for option " + "\"%s\"\n", tftp, name ); + return -EINVAL; + } + if ( ( rc = tftp_process_option ( tftp, name, value ) ) != 0 ) + return rc; + name = ( value + strlen ( value ) + 1 ); + } + + /* Mark as received block 0 (the OACK) */ + tftp_received ( tftp, 0 ); + + return 0; +} + +/** + * Receive DATA + * + * @v tftp TFTP connection + * @v buf Temporary data buffer + * @v len Length of temporary data buffer + * @ret rc Return status code + */ +static int tftp_rx_data ( struct tftp_session *tftp, void *buf, size_t len ) { + struct tftp_data *data = buf; + unsigned int block; + size_t data_len; + + /* Sanity check */ + if ( len < sizeof ( *data ) ) { + DBG ( "TFTP %p received underlength DATA packet length %d\n", + tftp, len ); + return -EINVAL; + } + + /* Pass to callback */ + block = ntohs ( data->block ); + data_len = ( len - offsetof ( typeof ( *data ), data ) ); + tftp->callback ( tftp, block, data->data, data_len ); + + /* Mark block as received */ + tftp_received ( tftp, block ); + + /* Finish when final block received */ + if ( data_len < tftp->blksize ) + tftp_done ( tftp, 0 ); + + return 0; +} + +/** + * Transmit ACK + * + * @v tftp TFTP connection + * @v buf Temporary data buffer + * @v len Length of temporary data buffer + * @ret rc Return status code + */ +static int tftp_send_ack ( struct tftp_session *tftp ) { + struct tftp_ack ack; + + ack.opcode = htons ( TFTP_ACK ); + ack.block = htons ( tftp->state ); + return udp_send ( &tftp->udp, &ack, sizeof ( ack ) ); +} + +/** + * Receive ERROR + * + * @v tftp TFTP connection + * @v buf Temporary data buffer + * @v len Length of temporary data buffer + * @ret rc Return status code + */ +static int tftp_rx_error ( struct tftp_session *tftp, void *buf, size_t len ) { + struct tftp_error *error = buf; + unsigned int err; + int rc = 0; + + /* Sanity check */ + if ( len < sizeof ( *error ) ) { + DBG ( "TFTP %p received underlength ERROR packet length %d\n", + tftp, len ); + return -EINVAL; + } + + DBG ( "TFTP %p received ERROR packet with code %d, message \"%s\"\n", + tftp, ntohs ( error->errcode ), error->errmsg ); + + /* Determine final operation result */ + err = ntohs ( error->errcode ); + if ( err < ( sizeof ( tftp_errors ) / sizeof ( tftp_errors[0] ) ) ) + rc = -tftp_errors[err]; + if ( ! rc ) + rc = -PXENV_STATUS_TFTP_CANNOT_OPEN_CONNECTION; + + /* Close TFTP session */ + tftp_done ( tftp, rc ); + + return 0; +} + +/** + * Transmit data + * + * @v conn UDP connection + * @v buf Temporary data buffer + * @v len Length of temporary data buffer + * @ret rc Return status code + */ +static int tftp_senddata ( struct udp_connection *conn, + void *buf, size_t len ) { + struct tftp_session *tftp = + container_of ( conn, struct tftp_session, udp ); + + if ( tftp->state < 0 ) { + return tftp_send_rrq ( tftp, buf, len ); + } else { + return tftp_send_ack ( tftp ); + } +} + +/** + * Receive new data + * + * @v udp UDP connection + * @v data Received data + * @v len Length of received data + * @v st_src Partially-filled source address + * @v st_dest Partially-filled destination address + */ +static int tftp_newdata ( struct udp_connection *conn, void *data, size_t len, + struct sockaddr_tcpip *st_src __unused, + struct sockaddr_tcpip *st_dest __unused ) { + struct tftp_session *tftp = + container_of ( conn, struct tftp_session, udp ); + struct tftp_common *common = data; + + if ( len < sizeof ( *common ) ) { + DBG ( "TFTP %p received underlength packet length %d\n", + tftp, len ); + return -EINVAL; + } + + /* Filter by TID. Set TID on first response received */ + if ( tftp->tid ) { + if ( tftp->tid != st_src->st_port ) { + DBG ( "TFTP %p received packet from wrong port " + "(got %d, wanted %d)\n", tftp, + ntohs ( st_src->st_port ), ntohs ( tftp->tid ) ); + return -EINVAL; + } + } else { + tftp->tid = st_src->st_port; + DBG ( "TFTP %p using remote port %d\n", tftp, + ntohs ( tftp->tid ) ); + udp_connect_port ( &tftp->udp, tftp->tid ); + } + + /* Filter by source address */ + if ( memcmp ( st_src, udp_peer ( &tftp->udp ), + sizeof ( *st_src ) ) != 0 ) { + DBG ( "TFTP %p received packet from foreign source\n", tftp ); + return -EINVAL; + } + + switch ( common->opcode ) { + case htons ( TFTP_OACK ): + return tftp_rx_oack ( tftp, data, len ); + case htons ( TFTP_DATA ): + return tftp_rx_data ( tftp, data, len ); + case htons ( TFTP_ERROR ): + return tftp_rx_error ( tftp, data, len ); + default: + DBG ( "TFTP %p received strange packet type %d\n", tftp, + ntohs ( common->opcode ) ); + return -EINVAL; + }; +} + +/** TFTP UDP operations */ +static struct udp_operations tftp_udp_operations = { + .senddata = tftp_senddata, + .newdata = tftp_newdata, +}; + +/** + * Initiate TFTP download + * + * @v tftp TFTP session + * @ret aop Asynchronous operation + */ +struct async_operation * tftp_get ( struct tftp_session *tftp ) { + int rc; + + assert ( tftp->filename != NULL ); + assert ( tftp->callback != NULL ); + assert ( tftp->udp.peer.st_family != 0 ); + + /* Initialise TFTP session */ + tftp->udp.udp_op = &tftp_udp_operations; + tftp->timer.expired = tftp_timer_expired; + tftp->state = -1; + tftp->blksize = TFTP_DEFAULT_BLKSIZE; + + /* Open UDP connection */ + if ( ( rc = udp_open ( &tftp->udp, 0 ) ) != 0 ) { + async_done ( &tftp->aop, rc ); + goto out; + } + + /* Transmit initial RRQ */ + tftp_send_packet ( tftp ); + + out: + return &tftp->aop; +} diff --git a/src/proto/tftp.c b/src/proto/tftp.c deleted file mode 100644 index 3f6bf35a..00000000 --- a/src/proto/tftp.c +++ /dev/null @@ -1,165 +0,0 @@ -#include "etherboot.h" -#include "proto.h" -#include "errno.h" -#include "tftp.h" -#include "tftpcore.h" - -/** @file - * - * TFTP protocol - */ - -/** - * Process a TFTP block - * - * @v state TFTP transfer state - * @v tftp_state::block Last received data block - * @v tftp_state::blksize Transfer block size - * @v data The data block to process - * @v buffer The buffer to fill with the data - * @ret True Block processed successfully - * @ret False Block not processed successfully - * @ret tftp_state::block Incremented if applicable - * @ret *eof End-of-file marker - * @err #PXENV_STATUS_TFTP_INVALID_PACKET_SIZE Packet is too large - * @err other As returned by fill_buffer() - * - * Process a TFTP DATA packet that has been received. If the data - * packet is the next data packet in the stream, its contents will be - * placed in the #buffer and tftp_state::block will be incremented. - * If the packet is the final packet, end-of-file will be indicated - * via #eof. - * - * If the data packet is a duplicate, then process_tftp_data() will - * still return True, though nothing will be done with the packet. A - * False return value always indicates an error that should abort the - * transfer. - */ -static inline int tftp_process_data ( struct tftp_state *state, - struct tftp_data *data, - struct buffer *buffer, - int *eof ) { - unsigned int blksize; - - /* Check it's the correct DATA block */ - if ( ntohs ( data->block ) != ( state->block + 1 ) ) { - DBG ( "TFTP: got block %d, wanted block %d\n", - ntohs ( data->block ), state->block + 1 ); - return 1; - } - /* Check it's an acceptable size */ - blksize = ( ntohs ( data->udp.len ) - + offsetof ( typeof ( *data ), udp ) - - offsetof ( typeof ( *data ), data ) ); - if ( blksize > state->blksize ) { - DBG ( "TFTP: oversized block size %d (max %d)\n", - blksize, state->blksize ); - errno = PXENV_STATUS_TFTP_INVALID_PACKET_SIZE; - return 0; - } - /* Place block in the buffer */ - if ( ! fill_buffer ( buffer, data->data, state->block * state->blksize, - blksize ) ) { - DBG ( "TFTP: could not place data in buffer: %m\n" ); - return 0; - } - /* Increment block counter */ - state->block++; - /* Set EOF marker */ - *eof = ( blksize < state->blksize ); - return 1; -} - -/** - * Download a file via TFTP - * - * @v server TFTP server - * @v file File name - * @v buffer Buffer into which to load file - * @ret True File was downloaded successfully - * @ret False File was not downloaded successfully - * @err #PXENV_STATUS_TFTP_UNKNOWN_OPCODE Unknown type of TFTP block received - * @err other As returned by tftp_open() - * @err other As returned by tftp_process_opts() - * @err other As returned by tftp_ack() - * @err other As returned by tftp_process_data() - * - * Download a file from a TFTP server into the specified buffer. - */ -static int tftp ( char *url __unused, struct sockaddr_in *server, char *file, - struct buffer *buffer ) { - struct tftp_state state; - union tftp_any *reply; - int eof = 0; - - /* Initialise TFTP state */ - memset ( &state, 0, sizeof ( state ) ); - state.server = *server; - - /* Open the file */ - if ( ! tftp_open ( &state, file, &reply, 0 ) ) { - DBG ( "TFTP: could not open %@:%d/%s : %m\n", - server->sin_addr.s_addr, server->sin_port, file ); - return 0; - } - - /* Fetch file, a block at a time */ - while ( 1 ) { - twiddle(); - switch ( ntohs ( reply->common.opcode ) ) { - case TFTP_DATA: - if ( ! tftp_process_data ( &state, &reply->data, - buffer, &eof ) ) { - tftp_error ( &state, TFTP_ERR_ILLEGAL_OP, - NULL ); - return 0; - } - break; - case TFTP_OACK: - if ( state.block ) { - /* OACK must be first block, if present */ - DBG ( "TFTP: OACK after block %d\n", - state.block ); - errno = PXENV_STATUS_TFTP_UNKNOWN_OPCODE; - tftp_error ( &state, TFTP_ERR_ILLEGAL_OP, - NULL ); - return 0; - } - if ( ! tftp_process_opts ( &state, &reply->oack ) ) { - DBG ( "TFTP: option processing failed: %m\n" ); - tftp_error ( &state, TFTP_ERR_BAD_OPTS, NULL ); - return 0; - } - break; - default: - DBG ( "TFTP: unexpected opcode %d\n", - ntohs ( reply->common.opcode ) ); - errno = PXENV_STATUS_TFTP_UNKNOWN_OPCODE; - tftp_error ( &state, TFTP_ERR_ILLEGAL_OP, NULL ); - return 0; - } - /* If we have reached EOF, stop here */ - if ( eof ) - break; - /* Fetch the next data block */ - if ( ! tftp_ack ( &state, &reply ) ) { - DBG ( "TFTP: could not get next block: %m\n" ); - if ( ! reply ) { - tftp_error ( &state, TFTP_ERR_ILLEGAL_OP, - NULL ); - } - return 0; - } - } - - /* ACK the final packet, as a courtesy to the server */ - tftp_ack_nowait ( &state ); - - return 1; -} - -struct protocol tftp_protocol __default_protocol = { - .name = "tftp", - .default_port = TFTP_PORT, - .load = tftp, -};