You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1138 lines
35 KiB
1138 lines
35 KiB
/* |
|
* Copyright (C) 2015 Nick van IJzendoorn <nijzendoorn@engineering-spirit.nl> |
|
* |
|
* This file is subject to the terms and conditions of the GNU Lesser General |
|
* Public License v2.1. See the file LICENSE in the top level directory for |
|
* more details. |
|
*/ |
|
|
|
/** |
|
* @ingroup net_gnrc_tftp |
|
* @{ |
|
* |
|
* @file |
|
* |
|
* @author Nick van IJzendoorn <nijzendoorn@engineering-spirit.nl> |
|
*/ |
|
|
|
#include <errno.h> |
|
#include <string.h> |
|
#include <stdio.h> |
|
#include <time.h> |
|
|
|
#include "net/gnrc/tftp.h" |
|
#include "net/gnrc/netapi.h" |
|
#include "net/gnrc/netreg.h" |
|
#include "net/gnrc/udp.h" |
|
#include "net/gnrc/ipv6.h" |
|
#include "random.h" |
|
|
|
#define ENABLE_DEBUG (1) |
|
#include "debug.h" |
|
|
|
#if ENABLE_DEBUG |
|
/* For PRIu16 etc. */ |
|
#include <inttypes.h> |
|
#endif |
|
|
|
static kernel_pid_t _tftp_kernel_pid; |
|
|
|
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ |
|
#define CT_HTONS(x) ((uint16_t)(( \ |
|
(((uint16_t)(x)) >> 8) & 0x00FF) | \ |
|
((((uint16_t)(x)) << 8) & 0xFF00))) |
|
#else |
|
#define CT_HTONS(x) ((uint16_t)x) |
|
#endif |
|
|
|
#define MIN(a, b) ((a) > (b) ? (b) : (a)) |
|
#define ARRAY_LEN(x) (sizeof(x) / sizeof(x[0])) |
|
|
|
#define TFTP_TIMEOUT_MSG 0x4000 |
|
#define TFTP_STOP_SERVER_MSG 0x4001 |
|
#define TFTP_DEFAULT_DATA_SIZE (GNRC_TFTP_MAX_TRANSFER_UNIT \ |
|
+ sizeof(tftp_packet_data_t)) |
|
|
|
/** |
|
* @brief TFTP mode help support |
|
*/ |
|
#define MODE(mode) { # mode, sizeof(# mode) } |
|
|
|
typedef struct { |
|
char *name; |
|
uint8_t len; |
|
} tftp_opt_t; |
|
|
|
/** |
|
* @brief TFTP opcodes |
|
* @{ |
|
*/ |
|
typedef uint16_t tftp_opcodes_t; |
|
|
|
#define TO_RRQ CT_HTONS(1) /**< Read Request */ |
|
#define TO_WRQ CT_HTONS(2) /**< Write Request */ |
|
#define TO_DATA CT_HTONS(3) /**< Data */ |
|
#define TO_ACK CT_HTONS(4) /**< Acknowledgment */ |
|
#define TO_ERROR CT_HTONS(5) /**< Error */ |
|
#define TO_OACK CT_HTONS(6) /**< Option ACK */ |
|
/** |
|
* @} |
|
*/ |
|
|
|
/** |
|
* @brief TFTP Error Codes |
|
* @{ |
|
*/ |
|
typedef uint16_t tftp_err_codes_t; |
|
|
|
#define TE_UN_DEF CT_HTONS(0) /**< Not defined, see error message */ |
|
#define TE_NO_FILE CT_HTONS(1) /**< File not found */ |
|
#define TE_ACCESS CT_HTONS(2) /**< Access violation */ |
|
#define TE_DFULL CT_HTONS(3) /**< Disk full or allocation exceeded */ |
|
#define TE_ILLOPT CT_HTONS(4) /**< Illegal TFTP operation */ |
|
#define TE_UNKOWN_ID CT_HTONS(5) /**< Unknown transfer ID */ |
|
#define TE_EXISTS CT_HTONS(6) /**< File already exists */ |
|
#define TE_UNKOWN_USR CT_HTONS(7) /**< No such user */ |
|
/** |
|
* @} |
|
*/ |
|
|
|
/* ordered as @see tftp_mode_t */ |
|
tftp_opt_t _tftp_modes[] = { |
|
[TTM_ASCII] = MODE(netascii), |
|
[TTM_OCTET] = MODE(octet), |
|
[TTM_MAIL] = MODE(mail), |
|
}; |
|
|
|
/** |
|
* @brief TFTP Options |
|
*/ |
|
typedef enum { |
|
TOPT_BLKSIZE, |
|
TOPT_TIMEOUT, |
|
TOPT_TSIZE, |
|
} tftp_options_t; |
|
|
|
/* ordered as @see tftp_options_t */ |
|
tftp_opt_t _tftp_options[] = { |
|
[TOPT_BLKSIZE] = MODE(blksize), |
|
[TOPT_TIMEOUT] = MODE(timeout), |
|
[TOPT_TSIZE] = MODE(tsize), |
|
}; |
|
|
|
/** |
|
* @brief The TFTP state |
|
*/ |
|
typedef enum { |
|
TS_FAILED = -1, |
|
TS_BUSY = 0, |
|
TS_FINISHED = 1 |
|
} tftp_state; |
|
|
|
/** |
|
* @brief The type of the context used |
|
*/ |
|
typedef enum { |
|
CT_SERVER, |
|
CT_CLIENT |
|
} tftp_context_type; |
|
|
|
/** |
|
* @brief The TFTP context for the current transfer |
|
*/ |
|
typedef struct { |
|
char file_name[GNRC_TFTP_MAX_FILENAME_LEN]; |
|
tftp_mode_t mode; |
|
tftp_opcodes_t op; |
|
ipv6_addr_t peer; |
|
xtimer_t timer; |
|
msg_t timer_msg; |
|
uint32_t timeout; |
|
uint16_t dst_port; |
|
uint16_t src_port; |
|
tftp_context_type ct; |
|
tftp_start_cb_t start_cb; |
|
tftp_data_cb_t data_cb; |
|
tftp_stop_cb_t stop_cb; |
|
gnrc_netreg_entry_t entry; |
|
|
|
/* transfer parameters */ |
|
uint16_t block_nr; |
|
uint16_t block_size; |
|
size_t transfer_size; |
|
uint32_t block_timeout; |
|
uint32_t retries; |
|
bool use_options; |
|
bool enable_options; |
|
bool write_finished; |
|
} tftp_context_t; |
|
|
|
/** |
|
* @brief The default TFTP header |
|
*/ |
|
typedef struct __attribute__((packed)) { |
|
tftp_opcodes_t opc; |
|
uint8_t data[]; |
|
} tftp_header_t; |
|
|
|
/** |
|
* @brief The TFTP data packet |
|
*/ |
|
typedef struct __attribute__((packed)) { |
|
tftp_opcodes_t opc; |
|
network_uint16_t block_nr; |
|
uint8_t data[]; |
|
} tftp_packet_data_t; |
|
|
|
/** |
|
* @brief The TFTP error packet |
|
*/ |
|
typedef struct __attribute__((packed)) { |
|
tftp_opcodes_t opc; |
|
tftp_err_codes_t err_code; |
|
char err_msg[]; |
|
} tftp_packet_error_t; |
|
|
|
/* get the TFTP opcode */ |
|
static inline tftp_opcodes_t _tftp_parse_type(uint8_t *buf) |
|
{ |
|
return ((tftp_header_t *)buf)->opc; |
|
} |
|
|
|
/* initialize the context to it's default state */ |
|
static int _tftp_init_ctxt(ipv6_addr_t *addr, const char *file_name, |
|
tftp_opcodes_t op, tftp_mode_t mode, tftp_context_type type, |
|
tftp_start_cb_t start, tftp_stop_cb_t stop, |
|
tftp_data_cb_t data, bool enable_options, tftp_context_t *ctxt); |
|
|
|
/* set the default TFTP options */ |
|
static void _tftp_set_default_options(tftp_context_t *ctxt); |
|
|
|
/* set the TFTP options to use */ |
|
static int _tftp_set_opts(tftp_context_t *ctxt, size_t blksize, uint32_t timeout, size_t total_size); |
|
|
|
/* this function registers the UDP port and won't return till the TFTP transfer is finished */ |
|
static int _tftp_do_client_transfer(tftp_context_t *ctxt); |
|
|
|
/* the state process of the TFTP transfer */ |
|
static tftp_state _tftp_state_processes(tftp_context_t *ctxt, msg_t *m); |
|
|
|
/* send an start request if we run in client mode */ |
|
static tftp_state _tftp_send_start(tftp_context_t *ctxt, gnrc_pktsnip_t *buf); |
|
|
|
/* send data or and ack depending if we are reading or writing */ |
|
static tftp_state _tftp_send_dack(tftp_context_t *ctxt, gnrc_pktsnip_t *buf, tftp_opcodes_t op); |
|
|
|
/* send and TFTP error to the client */ |
|
static tftp_state _tftp_send_error(tftp_context_t *ctxt, gnrc_pktsnip_t *buf, tftp_err_codes_t err, const char *err_msg); |
|
|
|
/* this function sends the actual packet */ |
|
static tftp_state _tftp_send(gnrc_pktsnip_t *buf, tftp_context_t *ctxt, size_t len); |
|
|
|
/* decode the default TFTP start packet */ |
|
static int _tftp_decode_start(tftp_context_t *ctxt, uint8_t *buf, gnrc_pktsnip_t *outbuf); |
|
|
|
/* decode the TFTP option extensions */ |
|
static int _tftp_decode_options(tftp_context_t *ctxt, gnrc_pktsnip_t *buf, uint32_t start); |
|
|
|
/* decode the received ACK packet */ |
|
static bool _tftp_validate_ack(tftp_context_t *ctxt, uint8_t *buf); |
|
|
|
/* processes the received data packet and calls the callback defined by the user */ |
|
static int _tftp_process_data(tftp_context_t *ctxt, gnrc_pktsnip_t *buf); |
|
|
|
/* decode the received error packet and calls the callback defined by the user */ |
|
static int _tftp_decode_error(uint8_t *buf, tftp_err_codes_t *err, const char * *err_msg); |
|
|
|
/* TFTP super loop server */ |
|
static int _tftp_server(tftp_context_t *ctxt); |
|
|
|
/* get the maximum allowed transfer unit to avoid 6Lo fragmentation */ |
|
static uint16_t _tftp_get_maximum_block_size(void) |
|
{ |
|
uint16_t tmp; |
|
kernel_pid_t ifs[GNRC_NETIF_NUMOF]; |
|
size_t ifnum = gnrc_netif_get(ifs); |
|
|
|
if (ifnum > 0 && gnrc_netapi_get(ifs[0], NETOPT_MAX_PACKET_SIZE, 0, &tmp, sizeof(uint16_t)) >= 0) { |
|
/* TODO calculate proper block size */ |
|
return tmp - sizeof(udp_hdr_t) - sizeof(ipv6_hdr_t) - 10; |
|
} |
|
|
|
return GNRC_TFTP_MAX_TRANSFER_UNIT; |
|
} |
|
|
|
int gnrc_tftp_client_read(ipv6_addr_t *addr, const char *file_name, tftp_mode_t mode, |
|
tftp_data_cb_t data_cb, tftp_start_cb_t start_cb, tftp_stop_cb_t stop_cb, bool use_option_extensions) |
|
{ |
|
tftp_context_t ctxt; |
|
|
|
assert(data_cb); |
|
assert(start_cb); |
|
assert(stop_cb); |
|
|
|
/* prepare the context */ |
|
if (_tftp_init_ctxt(addr, file_name, TO_RRQ, mode, CT_CLIENT, start_cb, stop_cb, data_cb, use_option_extensions, &ctxt) != TS_FINISHED) { |
|
return -EINVAL; |
|
} |
|
|
|
/* set the transfer options */ |
|
uint16_t mtu = _tftp_get_maximum_block_size(); |
|
if (!use_option_extensions || |
|
_tftp_set_opts(&ctxt, mtu, GNRC_TFTP_DEFAULT_TIMEOUT, 0) != TS_FINISHED) { |
|
_tftp_set_default_options(&ctxt); |
|
|
|
if (use_option_extensions) { |
|
return -EINVAL; |
|
} |
|
} |
|
|
|
/* start the process */ |
|
int ret = _tftp_do_client_transfer(&ctxt); |
|
|
|
/* remove possibly stale timer */ |
|
xtimer_remove(&(ctxt.timer)); |
|
|
|
return ret; |
|
} |
|
|
|
int gnrc_tftp_client_write(ipv6_addr_t *addr, const char *file_name, tftp_mode_t mode, |
|
tftp_data_cb_t data_cb, size_t total_size, tftp_stop_cb_t stop_cb, bool use_option_extensions) |
|
{ |
|
tftp_context_t ctxt; |
|
|
|
assert(data_cb); |
|
assert(stop_cb); |
|
|
|
/* prepare the context */ |
|
if (_tftp_init_ctxt(addr, file_name, TO_WRQ, mode, CT_CLIENT, NULL, stop_cb, data_cb, use_option_extensions, &ctxt) < 0) { |
|
return -EINVAL; |
|
} |
|
|
|
/* set the transfer options */ |
|
uint16_t mtu = _tftp_get_maximum_block_size(); |
|
if (!use_option_extensions || |
|
_tftp_set_opts(&ctxt, mtu, GNRC_TFTP_DEFAULT_TIMEOUT, total_size) != TS_FINISHED) { |
|
|
|
_tftp_set_default_options(&ctxt); |
|
|
|
if (use_option_extensions) { |
|
return -EINVAL; |
|
} |
|
} |
|
|
|
/* start the process */ |
|
int ret = _tftp_do_client_transfer(&ctxt); |
|
|
|
/* remove possibly stale timer */ |
|
xtimer_remove(&(ctxt.timer)); |
|
|
|
return ret; |
|
} |
|
|
|
int _tftp_init_ctxt(ipv6_addr_t *addr, const char *file_name, |
|
tftp_opcodes_t op, tftp_mode_t mode, tftp_context_type type, |
|
tftp_start_cb_t start, tftp_stop_cb_t stop, |
|
tftp_data_cb_t data, bool enable_options, tftp_context_t *ctxt) |
|
{ |
|
|
|
if (!addr) { |
|
return TS_FAILED; |
|
} |
|
|
|
memset(ctxt, 0, sizeof(*ctxt)); |
|
|
|
/* set the default context parameters */ |
|
ctxt->op = op; |
|
ctxt->ct = type; |
|
ctxt->data_cb = data; |
|
ctxt->start_cb = start; |
|
ctxt->stop_cb = stop; |
|
memcpy(&(ctxt->peer), addr, sizeof(ctxt->peer)); |
|
ctxt->mode = mode; |
|
if (file_name) { |
|
strncpy(ctxt->file_name, file_name, GNRC_TFTP_MAX_FILENAME_LEN); |
|
} |
|
ctxt->file_name[GNRC_TFTP_MAX_FILENAME_LEN - 1] = 0; |
|
ctxt->dst_port = GNRC_TFTP_DEFAULT_DST_PORT; |
|
ctxt->enable_options = enable_options; |
|
|
|
/* transport layer parameters */ |
|
ctxt->block_size = GNRC_TFTP_MAX_TRANSFER_UNIT; |
|
ctxt->block_timeout = GNRC_TFTP_DEFAULT_TIMEOUT; |
|
ctxt->write_finished = false; |
|
|
|
/* generate a random source UDP source port */ |
|
do { |
|
ctxt->src_port = (random_uint32() & 0xff) + GNRC_TFTP_DEFAULT_SRC_PORT; |
|
} while (gnrc_netreg_num(GNRC_NETTYPE_UDP, ctxt->src_port)); |
|
|
|
return TS_FINISHED; |
|
} |
|
|
|
void _tftp_set_default_options(tftp_context_t *ctxt) |
|
{ |
|
ctxt->block_size = GNRC_TFTP_MAX_TRANSFER_UNIT; |
|
ctxt->timeout = GNRC_TFTP_DEFAULT_TIMEOUT; |
|
ctxt->block_timeout = GNRC_TFTP_DEFAULT_TIMEOUT; |
|
ctxt->transfer_size = 0; |
|
ctxt->use_options = false; |
|
} |
|
|
|
int _tftp_set_opts(tftp_context_t *ctxt, size_t blksize, uint32_t timeout, size_t total_size) |
|
{ |
|
if (blksize > GNRC_TFTP_MAX_TRANSFER_UNIT || !timeout) { |
|
return TS_FAILED; |
|
} |
|
|
|
ctxt->block_size = blksize; |
|
ctxt->timeout = timeout; |
|
ctxt->block_timeout = timeout; |
|
ctxt->transfer_size = total_size; |
|
ctxt->use_options = true; |
|
|
|
return TS_FINISHED; |
|
} |
|
|
|
int gnrc_tftp_server(tftp_data_cb_t data_cb, tftp_start_cb_t start_cb, tftp_stop_cb_t stop_cb, bool use_options) |
|
{ |
|
/* check if there is only one TFTP server running */ |
|
if (_tftp_kernel_pid != KERNEL_PID_UNDEF) { |
|
DEBUG("tftp: only one TFTP server allowed\n"); |
|
return -1; |
|
} |
|
|
|
/* context will be initialized when a connection is established */ |
|
tftp_context_t ctxt; |
|
ctxt.data_cb = data_cb; |
|
ctxt.start_cb = start_cb; |
|
ctxt.stop_cb = stop_cb; |
|
ctxt.enable_options = use_options; |
|
|
|
/* validate our arguments */ |
|
assert(data_cb); |
|
assert(start_cb); |
|
assert(stop_cb); |
|
|
|
/* save our kernel PID */ |
|
_tftp_kernel_pid = thread_getpid(); |
|
|
|
/* start the server */ |
|
int ret = _tftp_server(&ctxt); |
|
|
|
/* remove possibly stale timer */ |
|
xtimer_remove(&(ctxt.timer)); |
|
|
|
/* reset the kernel PID */ |
|
_tftp_kernel_pid = KERNEL_PID_UNDEF; |
|
|
|
return ret; |
|
} |
|
|
|
int gnrc_tftp_server_stop(void) |
|
{ |
|
/* check if there is a server running */ |
|
if (_tftp_kernel_pid == KERNEL_PID_UNDEF) { |
|
DEBUG("tftp: no TFTP server running\n"); |
|
return -1; |
|
} |
|
|
|
/* prepare the stop message */ |
|
msg_t m = { |
|
thread_getpid(), |
|
TFTP_STOP_SERVER_MSG, |
|
{ NULL } |
|
}; |
|
|
|
/* send the stop message */ |
|
msg_send(&m, _tftp_kernel_pid); |
|
|
|
return 0; |
|
} |
|
|
|
int _tftp_server(tftp_context_t *ctxt) |
|
{ |
|
msg_t msg; |
|
bool active = true; |
|
gnrc_netreg_entry_t entry = GNRC_NETREG_ENTRY_INIT_PID(GNRC_TFTP_DEFAULT_DST_PORT, |
|
sched_active_pid); |
|
|
|
while (active) { |
|
int ret = TS_BUSY; |
|
bool got_client = false; |
|
|
|
/* register the servers main listening port */ |
|
if (gnrc_netreg_register(GNRC_NETTYPE_UDP, &entry)) { |
|
DEBUG("tftp: error starting server.\n"); |
|
return TS_FAILED; |
|
} |
|
|
|
/* main processing loop */ |
|
while (ret == TS_BUSY) { |
|
/* wait for a message */ |
|
msg_receive(&msg); |
|
|
|
/* check if the server stop message has been received */ |
|
if (msg.type == TFTP_STOP_SERVER_MSG) { |
|
active = false; |
|
ret = TS_FAILED; |
|
break; |
|
} |
|
else { |
|
/* continue normal server opration */ |
|
DEBUG("tftp: message incoming\n"); |
|
ret = _tftp_state_processes(ctxt, &msg); |
|
|
|
/* release packet if we received one */ |
|
if (msg.type == GNRC_NETAPI_MSG_TYPE_RCV) { |
|
gnrc_pktbuf_release(msg.content.ptr); |
|
} |
|
} |
|
|
|
/* if we just accepted a client, disable the server main listening port */ |
|
if (!got_client) { |
|
gnrc_netreg_unregister(GNRC_NETTYPE_UDP, &entry); |
|
|
|
if (ret == TS_BUSY) { |
|
got_client = true; |
|
DEBUG("tftp: connection established\n"); |
|
} |
|
} |
|
} |
|
|
|
/* remove any stall timers */ |
|
xtimer_remove(&(ctxt->timer)); |
|
|
|
/* if the server transfer has finished, unregister the client dst port */ |
|
gnrc_netreg_unregister(GNRC_NETTYPE_UDP, &(ctxt->entry)); |
|
DEBUG("tftp: connection terminated\n"); |
|
} |
|
|
|
/* unregister our UDP listener on this thread */ |
|
gnrc_netreg_unregister(GNRC_NETTYPE_UDP, &entry); |
|
|
|
return 0; |
|
} |
|
|
|
int _tftp_do_client_transfer(tftp_context_t *ctxt) |
|
{ |
|
msg_t msg; |
|
tftp_state ret = TS_BUSY; |
|
|
|
/* register our DNS response listener */ |
|
gnrc_netreg_entry_t entry = GNRC_NETREG_ENTRY_INIT_PID(ctxt->src_port, |
|
sched_active_pid); |
|
|
|
if (gnrc_netreg_register(GNRC_NETTYPE_UDP, &entry)) { |
|
DEBUG("tftp: error starting server.\n"); |
|
return TS_FAILED; |
|
} |
|
|
|
/* try to start the TFTP transfer */ |
|
ret = _tftp_state_processes(ctxt, NULL); |
|
if (ret != TS_BUSY) { |
|
DEBUG("tftp: transfer failed\n"); |
|
/* if the start failed return */ |
|
return ret; |
|
} |
|
|
|
/* main processing loop */ |
|
while (ret == TS_BUSY) { |
|
/* wait for a message */ |
|
msg_receive(&msg); |
|
DEBUG("tftp: message received\n"); |
|
ret = _tftp_state_processes(ctxt, &msg); |
|
|
|
/* release packet if we received one */ |
|
if (msg.type == GNRC_NETAPI_MSG_TYPE_RCV) { |
|
gnrc_pktbuf_release(msg.content.ptr); |
|
} |
|
} |
|
|
|
/* unregister our UDP listener on this thread */ |
|
gnrc_netreg_unregister(GNRC_NETTYPE_UDP, &entry); |
|
|
|
return ret; |
|
} |
|
|
|
tftp_state _tftp_state_processes(tftp_context_t *ctxt, msg_t *m) |
|
{ |
|
gnrc_pktsnip_t *outbuf = gnrc_pktbuf_add(NULL, NULL, TFTP_DEFAULT_DATA_SIZE, |
|
GNRC_NETTYPE_UNDEF); |
|
|
|
/* check if this is an client start */ |
|
if (!m) { |
|
DEBUG("tftp: starting transaction as client\n"); |
|
return _tftp_send_start(ctxt, outbuf); |
|
} |
|
else if (m->type == TFTP_TIMEOUT_MSG) { |
|
DEBUG("tftp: timeout occured\n"); |
|
if (++(ctxt->retries) > GNRC_TFTP_MAX_RETRIES) { |
|
/* transfer failed due to lost peer */ |
|
DEBUG("tftp: peer lost\n"); |
|
gnrc_pktbuf_release(outbuf); |
|
return TS_FAILED; |
|
} |
|
|
|
/* increase the timeout for congestion control */ |
|
ctxt->block_timeout <<= 1; |
|
|
|
/* the send message timed out, re-sending */ |
|
if (ctxt->dst_port == GNRC_TFTP_DEFAULT_DST_PORT) { |
|
DEBUG("tftp: sending timed out, re-sending\n"); |
|
/* we are still negotiating resent, start */ |
|
return _tftp_send_start(ctxt, outbuf); |
|
} |
|
else { |
|
DEBUG("tftp: last data or ack packet lost, resending\n"); |
|
/* we are sending / receiving data */ |
|
/* if we are reading resent the ACK, if writing the DATA */ |
|
return _tftp_send_dack(ctxt, outbuf, (ctxt->op == TO_RRQ) ? TO_ACK : TO_DATA); |
|
} |
|
} |
|
else if (m->type != GNRC_NETAPI_MSG_TYPE_RCV) { |
|
DEBUG("tftp: unknown message\n"); |
|
gnrc_pktbuf_release(outbuf); |
|
return TS_BUSY; |
|
} |
|
|
|
gnrc_pktsnip_t *pkt = m->content.ptr; |
|
|
|
gnrc_pktsnip_t *tmp; |
|
tmp = gnrc_pktsnip_search_type(pkt, GNRC_NETTYPE_UDP); |
|
udp_hdr_t *udp = (udp_hdr_t *)tmp->data; |
|
|
|
tmp = gnrc_pktsnip_search_type(pkt, GNRC_NETTYPE_IPV6); |
|
ipv6_hdr_t *ip = (ipv6_hdr_t *)tmp->data; |
|
uint8_t *data = (uint8_t *)pkt->data; |
|
|
|
xtimer_remove(&(ctxt->timer)); |
|
|
|
switch (_tftp_parse_type(data)) { |
|
case TO_RRQ: |
|
case TO_WRQ: { |
|
if (byteorder_ntohs(udp->dst_port) != GNRC_TFTP_DEFAULT_DST_PORT) { |
|
/* not a valid start packet */ |
|
DEBUG("tftp: incoming packet on port %d dropped\n", byteorder_ntohs(udp->dst_port)); |
|
gnrc_pktbuf_release(outbuf); |
|
return TS_FAILED; |
|
} |
|
|
|
/* reinitialize the context with the current client */ |
|
tftp_opcodes_t op = _tftp_parse_type(data); |
|
_tftp_init_ctxt(&(ip->src), NULL, op, TTM_ASCII, CT_SERVER, |
|
ctxt->start_cb, ctxt->stop_cb, ctxt->data_cb, |
|
ctxt->enable_options, ctxt); |
|
|
|
/* get the context of the client */ |
|
ctxt->dst_port = byteorder_ntohs(udp->src_port); |
|
DEBUG("tftp: client's port is %" PRIu16 "\n", ctxt->dst_port); |
|
|
|
int offset = _tftp_decode_start(ctxt, data, outbuf); |
|
DEBUG("tftp: offset after decode start = %i\n", offset); |
|
if (offset < 0) { |
|
DEBUG("tftp: there is no data?\n"); |
|
gnrc_pktbuf_release(outbuf); |
|
return TS_FAILED; |
|
} |
|
|
|
/* register a listener for the UDP port */ |
|
gnrc_netreg_entry_init_pid(&(ctxt->entry), ctxt->src_port, |
|
sched_active_pid); |
|
gnrc_netreg_register(GNRC_NETTYPE_UDP, &(ctxt->entry)); |
|
|
|
/* try to decode the options */ |
|
tftp_state state; |
|
tftp_opcodes_t opcode; |
|
if (ctxt->enable_options && |
|
_tftp_decode_options(ctxt, pkt, offset) > offset) { |
|
DEBUG("tftp: send option ACK\n"); |
|
|
|
/* the client send the TFTP options */ |
|
opcode = TO_OACK; |
|
} |
|
else { |
|
DEBUG("tftp: send normal ACK\n"); |
|
|
|
/* the client didn't send options, use ACK and set defaults */ |
|
_tftp_set_default_options(ctxt); |
|
|
|
/* send the first data block */ |
|
if (ctxt->op == TO_RRQ) { |
|
++(ctxt->block_nr); |
|
opcode = TO_DATA; |
|
} |
|
else { |
|
opcode = TO_ACK; |
|
} |
|
} |
|
|
|
/* validate if the application accepts the action, mode, filename and transfer_size */ |
|
tftp_action_t action = (ctxt->op == TO_RRQ) ? TFTP_READ : TFTP_WRITE; |
|
if (!ctxt->start_cb(action, ctxt->mode, ctxt->file_name, &(ctxt->transfer_size))) { |
|
_tftp_send_error(ctxt, outbuf, TE_ACCESS, "Blocked by user application"); |
|
DEBUG("tftp: callback not able to handle mode\n"); |
|
return TS_FAILED; |
|
} |
|
|
|
/* the client send the TFTP options */ |
|
state = _tftp_send_dack(ctxt, outbuf, opcode); |
|
|
|
/* check if the client negotiation was successful */ |
|
if (state != TS_BUSY) { |
|
DEBUG("tftp: not able to send ACK\n"); |
|
} |
|
return state; |
|
} break; |
|
|
|
case TO_DATA: { |
|
/* try to process the data */ |
|
int proc = _tftp_process_data(ctxt, pkt); |
|
if (proc < 0) { |
|
DEBUG("tftp: data not accepted\n"); |
|
/* the data is not accepted return */ |
|
gnrc_pktbuf_release(outbuf); |
|
return TS_BUSY; |
|
} |
|
|
|
/* check if this is the first block */ |
|
if (!ctxt->block_nr |
|
&& ctxt->dst_port == GNRC_TFTP_DEFAULT_DST_PORT |
|
&& !ctxt->use_options) { |
|
/* no OACK received, restore default TFTP parameters */ |
|
_tftp_set_default_options(ctxt); |
|
DEBUG("tftp: restore default TFTP parameters\n"); |
|
|
|
/* switch the destination port to the src port of the server */ |
|
ctxt->dst_port = byteorder_ntohs(udp->src_port); |
|
} |
|
|
|
/* wait for the next data block */ |
|
DEBUG("tftp: wait for the next data block\n"); |
|
++(ctxt->block_nr); |
|
_tftp_send_dack(ctxt, outbuf, TO_ACK); |
|
|
|
/* check if the data transfer has finished */ |
|
if (proc < ctxt->block_size) { |
|
DEBUG("tftp: transfer finished\n"); |
|
|
|
if (ctxt->stop_cb) { |
|
ctxt->stop_cb(TFTP_SUCCESS, NULL); |
|
} |
|
|
|
return TS_FINISHED; |
|
} |
|
|
|
return TS_BUSY; |
|
} break; |
|
|
|
case TO_ACK: { |
|
/* validate if this is the ACK we are waiting for */ |
|
if (!_tftp_validate_ack(ctxt, data)) { |
|
/* invalid packet ACK, drop */ |
|
gnrc_pktbuf_release(outbuf); |
|
return TS_BUSY; |
|
} |
|
|
|
/* check if the write action is finished */ |
|
if (ctxt->write_finished) { |
|
gnrc_pktbuf_release(outbuf); |
|
|
|
if (ctxt->stop_cb) { |
|
ctxt->stop_cb(TFTP_SUCCESS, NULL); |
|
} |
|
|
|
return TS_FINISHED; |
|
} |
|
|
|
/* check if this is the first ACK */ |
|
if (!ctxt->block_nr && ctxt->dst_port != byteorder_ntohs(udp->src_port)) { |
|
/* no OACK received restore default TFTP parameters */ |
|
_tftp_set_default_options(ctxt); |
|
|
|
/* switch the destination port to the src port of the server */ |
|
ctxt->dst_port = byteorder_ntohs(udp->src_port); |
|
} |
|
|
|
/* send the next data block */ |
|
++(ctxt->block_nr); |
|
|
|
return _tftp_send_dack(ctxt, outbuf, TO_DATA); |
|
} break; |
|
|
|
case TO_ERROR: { |
|
tftp_err_codes_t err; |
|
const char *err_msg; |
|
|
|
/* decode the received error */ |
|
_tftp_decode_error(data, &err, &err_msg); |
|
|
|
/* inform the user application about the error */ |
|
if (ctxt->stop_cb) { |
|
ctxt->stop_cb(TFTP_PEER_ERROR, err_msg); |
|
} |
|
|
|
DEBUG("tftp: ERROR: %s\n", err_msg); |
|
gnrc_pktbuf_release(outbuf); |
|
return TS_FAILED; |
|
} break; |
|
|
|
case TO_OACK: { |
|
/* only allow one OACK to be received */ |
|
if (ctxt->dst_port != byteorder_ntohs(udp->src_port)) { |
|
DEBUG("tftp: TO_OACK received\n"); |
|
|
|
/* decode the options */ |
|
_tftp_decode_options(ctxt, pkt, 0); |
|
|
|
/* take the new source port */ |
|
ctxt->dst_port = byteorder_ntohs(udp->src_port); |
|
|
|
/* we must send block one to finish the negotiation in send mode */ |
|
if (ctxt->op == TO_WRQ) { |
|
++(ctxt->block_nr); |
|
} |
|
} |
|
else { |
|
DEBUG("tftp: dropping double TO_OACK\n"); |
|
} |
|
|
|
return _tftp_send_dack(ctxt, outbuf, (ctxt->op == TO_WRQ) ? TO_DATA : TO_ACK); |
|
} break; |
|
} |
|
|
|
gnrc_pktbuf_release(outbuf); |
|
return TS_FAILED; |
|
} |
|
|
|
size_t _tftp_add_option(uint8_t *dst, tftp_opt_t *opt, uint32_t value) |
|
{ |
|
size_t offset; |
|
|
|
/* set the option name */ |
|
memcpy(dst, opt->name, opt->len); |
|
offset = opt->len; |
|
|
|
/* set the option value */ |
|
offset += sprintf((char *)(dst + opt->len), "%" PRIu32, value); |
|
|
|
/* finish option value */ |
|
*(dst + offset) = 0; |
|
return ++offset; |
|
} |
|
|
|
uint32_t _tftp_append_options(tftp_context_t *ctxt, tftp_header_t *hdr, uint32_t offset) |
|
{ |
|
offset += _tftp_add_option(hdr->data + offset, _tftp_options + TOPT_BLKSIZE, ctxt->block_size); |
|
offset += _tftp_add_option(hdr->data + offset, _tftp_options + TOPT_TIMEOUT, (ctxt->timeout / US_PER_SEC)); |
|
|
|
/** |
|
* Only set the transfer option if we are sending. |
|
* Or when we are reading in bin mode. |
|
*/ |
|
if ((ctxt->ct == CT_SERVER && ctxt->op == TO_RRQ) || |
|
(ctxt->ct == CT_CLIENT && ctxt->op == TO_WRQ) || |
|
ctxt->mode == TTM_OCTET) { |
|
offset += _tftp_add_option(hdr->data + offset, _tftp_options + TOPT_TSIZE, ctxt->transfer_size); |
|
} |
|
|
|
return offset; |
|
} |
|
|
|
tftp_state _tftp_send_start(tftp_context_t *ctxt, gnrc_pktsnip_t *buf) |
|
{ |
|
/* get required values */ |
|
int len = strlen(ctxt->file_name) + 1; /* we also want the \0 char */ |
|
tftp_opt_t *m = _tftp_modes + ctxt->mode; |
|
|
|
/* start filling the header */ |
|
tftp_header_t *hdr = (tftp_header_t *)(buf->data); |
|
|
|
hdr->opc = ctxt->op; |
|
memcpy(hdr->data, ctxt->file_name, len); |
|
memcpy(hdr->data + len, m->name, m->len); |
|
|
|
/* fill the options */ |
|
uint32_t offset = (len + m->len); |
|
if (ctxt->use_options) { |
|
offset = _tftp_append_options(ctxt, hdr, offset); |
|
} |
|
|
|
/* send the data */ |
|
return _tftp_send(buf, ctxt, offset + sizeof(tftp_header_t)); |
|
} |
|
|
|
tftp_state _tftp_send_dack(tftp_context_t *ctxt, gnrc_pktsnip_t *buf, tftp_opcodes_t op) |
|
{ |
|
size_t len = 0; |
|
|
|
assert(op == TO_DATA || op == TO_ACK || op == TO_OACK); |
|
|
|
/* fill the packet */ |
|
tftp_packet_data_t *pkt = (tftp_packet_data_t *)(buf->data); |
|
pkt->block_nr = byteorder_htons(ctxt->block_nr); |
|
pkt->opc = op; |
|
|
|
if (op == TO_DATA) { |
|
DEBUG("tftp: getting data from callback\n"); |
|
/* get the required data from the user */ |
|
len = ctxt->data_cb(ctxt->block_size * (ctxt->block_nr - 1), pkt->data, ctxt->block_size); |
|
|
|
/* check if we are finished on ACK receive */ |
|
ctxt->write_finished = (len < ctxt->block_size); |
|
|
|
/* enable timeout */ |
|
ctxt->block_timeout = ctxt->timeout; |
|
} |
|
else if (op == TO_OACK) { |
|
/* append the options */ |
|
len = _tftp_append_options(ctxt, (tftp_header_t *)pkt, 0); |
|
|
|
/* disable timeout*/ |
|
ctxt->block_timeout = 0; |
|
} |
|
else if (op == TO_ACK) { |
|
/* disable timeout*/ |
|
ctxt->block_timeout = 0; |
|
} |
|
|
|
/* send the data */ |
|
return _tftp_send(buf, ctxt, sizeof(tftp_packet_data_t) + len); |
|
} |
|
|
|
tftp_state _tftp_send_error(tftp_context_t *ctxt, gnrc_pktsnip_t *buf, tftp_err_codes_t err, const char *err_msg) |
|
{ |
|
int strl = err_msg |
|
? strlen(err_msg) + 1 |
|
: 0; |
|
|
|
(void) ctxt; |
|
|
|
/* fill the packet */ |
|
tftp_packet_error_t *pkt = (tftp_packet_error_t *)(buf->data); |
|
pkt->opc = TO_ERROR; |
|
pkt->err_code = err; |
|
memcpy(pkt->err_msg, err_msg, strl); |
|
|
|
/* don't set a timeout on the error */ |
|
ctxt->block_timeout = 0; |
|
|
|
/* return the size of the packet */ |
|
_tftp_send(buf, ctxt, sizeof(tftp_packet_error_t) + strl); |
|
|
|
return TS_FAILED; |
|
} |
|
|
|
tftp_state _tftp_send(gnrc_pktsnip_t *buf, tftp_context_t *ctxt, size_t len) |
|
{ |
|
network_uint16_t src_port, dst_port; |
|
gnrc_pktsnip_t *udp, *ip; |
|
|
|
assert(len <= TFTP_DEFAULT_DATA_SIZE); |
|
|
|
/* down-size the packet to it's used size */ |
|
if (len > TFTP_DEFAULT_DATA_SIZE) { |
|
DEBUG("tftp: can't reallocate to bigger packet, buffer overflowed\n"); |
|
gnrc_pktbuf_release(buf); |
|
|
|
if (ctxt->stop_cb) { |
|
ctxt->stop_cb(TFTP_INTERN_ERROR, "buffer overflowed"); |
|
} |
|
|
|
return TS_FAILED; |
|
} |
|
else if (gnrc_pktbuf_realloc_data(buf, len) != 0) { |
|
assert(false); |
|
|
|
DEBUG("tftp: failed to reallocate data snippet\n"); |
|
gnrc_pktbuf_release(buf); |
|
|
|
/* inform the user that we can't reallocate */ |
|
if (ctxt->stop_cb) { |
|
ctxt->stop_cb(TFTP_INTERN_ERROR, "no reallocate"); |
|
} |
|
|
|
return TS_FAILED; |
|
} |
|
|
|
/* allocate UDP header, set source port := destination port */ |
|
src_port.u16 = ctxt->src_port; |
|
dst_port.u16 = ctxt->dst_port; |
|
udp = gnrc_udp_hdr_build(buf, src_port.u16, dst_port.u16); |
|
if (udp == NULL) { |
|
DEBUG("tftp: error unable to allocate UDP header\n"); |
|
gnrc_pktbuf_release(buf); |
|
|
|
if (ctxt->stop_cb) { |
|
ctxt->stop_cb(TFTP_INTERN_ERROR, "no udp allocate"); |
|
} |
|
|
|
return TS_FAILED; |
|
} |
|
|
|
/* allocate IPv6 header */ |
|
ip = gnrc_ipv6_hdr_build(udp, NULL, &(ctxt->peer)); |
|
if (ip == NULL) { |
|
DEBUG("tftp: error unable to allocate IPv6 header\n"); |
|
gnrc_pktbuf_release(udp); |
|
|
|
if (ctxt->stop_cb) { |
|
ctxt->stop_cb(TFTP_INTERN_ERROR, "no ip allocate"); |
|
} |
|
|
|
return TS_FAILED; |
|
} |
|
|
|
/* send packet */ |
|
if (gnrc_netapi_dispatch_send(GNRC_NETTYPE_UDP, GNRC_NETREG_DEMUX_CTX_ALL, |
|
ip) == 0) { |
|
/* if send failed inform the user */ |
|
DEBUG("tftp: error unable to locate UDP thread\n"); |
|
gnrc_pktbuf_release(ip); |
|
|
|
if (ctxt->stop_cb) { |
|
ctxt->stop_cb(TFTP_INTERN_ERROR, "no dispatch send"); |
|
} |
|
|
|
return TS_FAILED; |
|
} |
|
|
|
/* only set timeout if enabled for this block */ |
|
if (ctxt->block_timeout) { |
|
ctxt->timer_msg.type = TFTP_TIMEOUT_MSG; |
|
xtimer_set_msg(&(ctxt->timer), ctxt->block_timeout, &(ctxt->timer_msg), thread_getpid()); |
|
DEBUG("tftp: set timeout %" PRIu32 " ms\n", ctxt->block_timeout / US_PER_MS); |
|
} |
|
|
|
return TS_BUSY; |
|
} |
|
|
|
bool _tftp_validate_ack(tftp_context_t *ctxt, uint8_t *buf) |
|
{ |
|
tftp_packet_data_t *pkt = (tftp_packet_data_t *) buf; |
|
|
|
return ctxt->block_nr == byteorder_ntohs(pkt->block_nr); |
|
} |
|
|
|
int _tftp_decode_start(tftp_context_t *ctxt, uint8_t *buf, gnrc_pktsnip_t *outbuf) |
|
{ |
|
/* decode the packet */ |
|
tftp_header_t *hdr = (tftp_header_t *)buf; |
|
|
|
/* get the file name and copy terminating byte */ |
|
size_t fnlen = strlen((char *)hdr->data) + 1; |
|
|
|
if (fnlen >= GNRC_TFTP_MAX_FILENAME_LEN) { |
|
_tftp_send_error(ctxt, outbuf, TE_ILLOPT, "Filename to long"); |
|
|
|
if (ctxt->stop_cb) { |
|
ctxt->stop_cb(TFTP_INTERN_ERROR, "Filename to long"); |
|
} |
|
|
|
return TS_FAILED; |
|
} |
|
memcpy(ctxt->file_name, hdr->data, fnlen); |
|
|
|
/* Get mode string by advancing pointer */ |
|
char *str_mode = (char *)hdr->data + fnlen; |
|
DEBUG("tftp: incoming request '%s', mode: %s\n", ctxt->file_name, str_mode); |
|
|
|
/* decode the TFTP transfer mode */ |
|
for (uint32_t idx = 0; idx < ARRAY_LEN(_tftp_modes); ++idx) { |
|
if (memcmp(_tftp_modes[idx].name, str_mode, _tftp_modes[idx].len) == 0) { |
|
ctxt->mode = (tftp_mode_t)idx; |
|
return (str_mode + _tftp_modes[idx].len) - (char *)hdr->data; |
|
} |
|
} |
|
|
|
return -EINVAL; |
|
} |
|
|
|
int _tftp_decode_options(tftp_context_t *ctxt, gnrc_pktsnip_t *buf, uint32_t start) |
|
{ |
|
tftp_header_t *pkt = (tftp_header_t *)buf->data; |
|
size_t offset = start; |
|
|
|
DEBUG("tftp: decode options\n"); |
|
DEBUG("tftp: buffer size = %lu\n", (unsigned long)buf->size); |
|
while ((offset + sizeof(uint16_t)) < (buf->size)) { |
|
DEBUG("tftp: offset = %lu\n", (unsigned long)offset); |
|
/* get the option name */ |
|
const char *name = (const char *)(pkt->data + offset); |
|
offset += strlen(name) + 1; |
|
/* get the value name */ |
|
const char *value = (const char *)(pkt->data + offset); |
|
offset += strlen(value) + 1; |
|
|
|
/* check what option we are parsing */ |
|
for (uint32_t idx = 0; idx < ARRAY_LEN(_tftp_options); ++idx) { |
|
if (memcmp(name, _tftp_options[idx].name, _tftp_options[idx].len) == 0) { |
|
/* set the option value of the known options */ |
|
switch (idx) { |
|
case TOPT_BLKSIZE: |
|
ctxt->block_size = atoi(value); |
|
DEBUG("tftp: got option TOPT_BLKSIZE = %" PRIu16 "\n", ctxt->block_size); |
|
break; |
|
|
|
case TOPT_TSIZE: |
|
ctxt->transfer_size = atoi(value); |
|
DEBUG("tftp: got option TOPT_TSIZE = %" PRIu32 "\n", (uint32_t)ctxt->transfer_size); |
|
|
|
if (ctxt->start_cb && ctxt->ct == CT_CLIENT) { |
|
ctxt->start_cb(TFTP_READ, ctxt->mode, ctxt->file_name, &(ctxt->transfer_size)); |
|
} |
|
break; |
|
|
|
case TOPT_TIMEOUT: |
|
ctxt->timeout = atoi(value) * US_PER_SEC; |
|
DEBUG("tftp: option TOPT_TIMEOUT = %" PRIu32 " ms\n", ctxt->timeout / US_PER_MS); |
|
break; |
|
} |
|
|
|
break; |
|
} |
|
} |
|
} |
|
|
|
DEBUG("tftp: return %lu\n", (unsigned long)offset); |
|
return offset; |
|
} |
|
|
|
int _tftp_process_data(tftp_context_t *ctxt, gnrc_pktsnip_t *buf) |
|
{ |
|
tftp_packet_data_t *pkt = (tftp_packet_data_t *) buf->data; |
|
|
|
DEBUG("tftp: processing data\n"); |
|
|
|
uint16_t block_nr = byteorder_ntohs(pkt->block_nr); |
|
|
|
/* check if this is the packet we are waiting for */ |
|
if (block_nr != (ctxt->block_nr + 1)) { |
|
DEBUG("tftp: not the packet we were wating for\n"); |
|
return TS_BUSY; |
|
} |
|
|
|
/* send the user data trough to the user application */ |
|
if (ctxt->data_cb(ctxt->block_nr * ctxt->block_size, pkt->data, buf->size - sizeof(tftp_packet_data_t)) < 0) { |
|
DEBUG("tftp: error in data callback\n"); |
|
return TS_BUSY; |
|
} |
|
|
|
/* return the number of data bytes received */ |
|
return buf->size - sizeof(tftp_packet_data_t); |
|
} |
|
|
|
int _tftp_decode_error(uint8_t *buf, tftp_err_codes_t *err, const char * *err_msg) |
|
{ |
|
tftp_packet_error_t *pkt = (tftp_packet_error_t *) buf; |
|
|
|
/* return the error code and message */ |
|
*err = pkt->err_code; |
|
*err_msg = pkt->err_msg; |
|
|
|
return sizeof(tftp_packet_error_t) + strlen(pkt->err_msg) + 1; |
|
} |
|
|
|
/** |
|
* @} |
|
*/
|
|
|