diff --git a/Makefile.dep b/Makefile.dep index db312db47c297fdbe02be66984cb3f6fbd2b507f..02c957b9c9700f255825d4dfbe8e7fc9c469628d 100644 --- a/Makefile.dep +++ b/Makefile.dep @@ -43,6 +43,11 @@ ifneq (,$(filter gnrc_zep,$(USEMODULE))) USEMODULE += vtimer endif +ifneq (,$(filter gnrc_tftp,$(USEMODULE))) + USEMODULE += gnrc_udp + USEMODULE += xtimer +endif + ifneq (,$(filter gnrc_rpl,$(USEMODULE))) USEMODULE += fib USEMODULE += gnrc_ipv6_router_default diff --git a/examples/gnrc_tftp/Makefile b/examples/gnrc_tftp/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..b92f8e951c83b361e7959bfe8c6aa841cc9b574c --- /dev/null +++ b/examples/gnrc_tftp/Makefile @@ -0,0 +1,42 @@ +# name of your application +APPLICATION = gnrc_tftp + +# If no BOARD is found in the environment, use this default: +BOARD ?= native + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../.. + +BOARD_INSUFFICIENT_MEMORY := airfy-beacon chronos msb-430 msb-430h nrf51dongle \ + nrf6310 nucleo-f334 pca10000 pca10005 spark-core \ + stm32f0discovery telosb weio wsn430-v1_3b wsn430-v1_4 \ + yunjia-nrf51822 z1 + +# Include packages that pull up and auto-init the link layer. +# NOTE: 6LoWPAN will be included if IEEE802.15.4 devices are present +USEMODULE += gnrc_netif_default +USEMODULE += auto_init_gnrc_netif +# Specify the mandatory networking modules for IPv6 and UDP +USEMODULE += gnrc_ipv6_router_default +USEMODULE += gnrc_udp +# Add a routing protocol +USEMODULE += gnrc_rpl +# This application dumps received packets to STDIO using the pktdump module +USEMODULE += gnrc_pktdump +# Additional networking modules that can be dropped if not needed +USEMODULE += gnrc_icmpv6_echo +USEMODULE += gnrc_tftp +# Add also the shell, some shell commands +USEMODULE += shell +USEMODULE += shell_commands +USEMODULE += ps + +# Comment this out to disable code in RIOT that does safety checking +# which is not needed in a production environment but helps in the +# development process: +CFLAGS += -DDEVELHELP + +# Change this to 0 show compiler invocation lines by default: +QUIET ?= 1 + +include $(RIOTBASE)/Makefile.include diff --git a/examples/gnrc_tftp/README.md b/examples/gnrc_tftp/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2627976a2e8cb951d99c588bfd35a253f497200b --- /dev/null +++ b/examples/gnrc_tftp/README.md @@ -0,0 +1,86 @@ +# gnrc_tftp example + +## Connecting RIOT native and the Linux host + +> **Note:** RIOT does not support IPv4, so you need to stick to IPv6 anytime. To establish a connection between RIOT and the Linux host, +you will need `tftp` (with IPv6 support). On Ubuntu and Debian you would need the package `tftp-hpa` for TFTP client and `tftpd-hpa` for TFTP server. +Be aware that many programs require you to add an option such as -6 to tell them to use IPv6, otherwise they +will fail. If you're using a _Raspberry Pi_, run `sudo modprobe ipv6` before trying this example, because raspbian does not load the +IPv6 module automatically. +On some systems (openSUSE for example), the _firewall_ may interfere, and prevent some packets to arrive at the application (they will +however show up in Wireshark, which can be confusing). So be sure to adjust your firewall rules, or turn it off (who needs security anyway). + +First, create a tap interface (to which RIOT will connect) and a bridge (to which Linux will connect) from the RIOT main directory run: + + ./dist/tools/tapsetup/tapsetup -c 1 + +Now you can start the `gnrc_tftp` example by invoking `make term`. This should automatically connect to the `tap0` interface. If +this doesn't work for some reason, run `make` without any arguments, and then run the binary manually like so (assuming you are in the `examples/gnrc_tftp` directory): + +To verify that there is connectivity between RIOT and Linux, go to the RIOT console and run `ifconfig`: + + > ifconfig + Iface 6 HWaddr: 7e:ed:d2:ee:e1:07 + MTU:1280 + Source address length: 6 + Link type: wired + inet6 addr: ff02::1/128 scope: local [multicast] + inet6 addr: fe80::7ced:d2ff:feee:e107/64 scope: local + inet6 addr: ff02::1:ffee:e107/128 scope: local [multicast] + + +Copy the [link-local address](https://en.wikipedia.org/wiki/Link-local_address) of the RIOT node (prefixed with `fe80`) and try to ping it **from the Linux node**: + + ping6 fe80::7ced:d2ff:feee:e107%tapbr0 + +Note that the interface on which to send the ping needs to be appended to the IPv6 address, `%tapbr0` in the above example. When talking to the RIOT node, you always want to send to/receive from the `tapbr0` interface. + +If the pings succeed you can go on to send UDP packets. To do that, first start a UDP server on the RIOT node: + + > tftps start + tftp_server: Starting TFTP service at port 69 + +Now, on the Linux host, you can run tftp to connect with RIOT's TFTP server: + + $ tftp -v -6 fe80::7ced:d2ff:feee:e107%tapbr0 -c get welcome.txt + +The output will be something like: + + Connected to fe80::7ced:d2ff:feee:e107%tapbr0 (fe80::7ced:d2ff:feee:e107), port 69 + getting from fe80::7ced:d2ff:feee:e107%tapbr0:welcome.txt to welcome.txt [netascii] + Received 94 bytes in 0.0 seconds [113425 bit/s] + +The `-6` option is necessary to tell tftp to use IPv6 only and the `-v` is to tell the tftp client to be verbose. + +You should now see that the TFTP messages are received on the RIOT side. Opening a TFTP server on the Linux side is also possible. To do that, write down the IP address of the host (run on Linux): + + ifconfig tapbr0 + tapbr0 Link encap:Ethernet HWaddr 0e:bc:0f:49:7f:e4 + inet6 addr: fe80::cbc:fff:fe49:7fe4/64 Scope:Link + UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 + RX packets:22 errors:0 dropped:0 overruns:0 frame:0 + TX packets:53 errors:0 dropped:0 overruns:0 carrier:0 + collisions:0 txqueuelen:0 + RX bytes:1928 (1.9 KB) TX bytes:6217 (6.2 KB) + +And start the tftp server on Linux: + + $ sudo /etc/init.d/tftpd-hpa stop && mkdir -p tftpserv && echo "hello world" > tftpserv/welcome.txt && sudo in.tftpd -vvv -L -6 -c -s -u ${USER} ./tftpserv + +Now, on the RIOT side, send a UDP packet using: + + tftpc get welcome.txt octet 1 fe80::cbc:fff:fe49:7fe4 + +You will get output that looks like this: + + tftp_client: bin read welcome.txt:12 + + -- CLIENT DATA -- + hello worl + -- CLIENT DATA -- + + -- CLIENT DATA -- + d + + -- CLIENT DATA -- + tftp_client: SUCCESS: (null) diff --git a/examples/gnrc_tftp/main.c b/examples/gnrc_tftp/main.c new file mode 100644 index 0000000000000000000000000000000000000000..e287aa21267be3e9ab936e19683fb20616f033eb --- /dev/null +++ b/examples/gnrc_tftp/main.c @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 Engineering-Spirit + * + * 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 examples + * @{ + * + * @file + * @brief Example application for demonstrating the RIOT TFTP stack + * + * @author Nick van IJzendoorn <nijzendoorn@engineering-spirit.nl> + * + * @} + */ + +#include <unistd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "shell.h" +#include "msg.h" + +extern int tftp_client_cmd(int argc, char * *argv); +extern int tftp_server_cmd(int argc, char * *argv); + +#define MAIN_QUEUE_SIZE (4) +static msg_t _main_msg_queue[MAIN_QUEUE_SIZE]; + +static const shell_command_t shell_commands[] = { + { "tftpc", "get/put data to a TFTP server", tftp_client_cmd }, + { "tftps", "start and stop the TFTP server", tftp_server_cmd }, + { NULL, NULL, NULL } +}; + +int main(void) +{ + /* we need a message queue for the thread running the shell in order to + * receive potentially fast incoming networking packets */ + msg_init_queue(_main_msg_queue, MAIN_QUEUE_SIZE); + puts("RIOT TFTP stack example application"); + + /* start shell */ + puts("All up, running the shell now"); + char line_buf[SHELL_DEFAULT_BUFSIZE]; + + shell_run(shell_commands, line_buf, SHELL_DEFAULT_BUFSIZE); + + /* should be never reached */ + return 0; +} diff --git a/examples/gnrc_tftp/tftp_client.c b/examples/gnrc_tftp/tftp_client.c new file mode 100644 index 0000000000000000000000000000000000000000..5f36fd93873c21f19e3166822f94feb3cf430472 --- /dev/null +++ b/examples/gnrc_tftp/tftp_client.c @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2015 Engineering-Spirit + * + * 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 examples + * @{ + * + * @file + * @brief Demonstrating the sending and receiving of data via the TFTP client + * + * @author Nick van IJzendoorn <nijzendoorn@engineering-spirit.nl> + * + * @} + */ + +#include <stdio.h> +#include <string.h> +#include <inttypes.h> + +#include "kernel.h" +#include "net/gnrc/tftp.h" + +static const char *_tftp_default_host = "::1"; + +/* default server text which can be received */ +static const char _tftp_client_hello[] = "Hello,\n" + "\n" + "Client text would also need to exist to be able to put data.\n" + "\n" + "Enjoy the RIOT-OS\n"; + +static tftp_action_t _tftp_action; + +/** + * @brief called at every transaction start + */ +static bool _tftp_client_start_cb(tftp_action_t action, tftp_mode_t mode, + const char *file_name, size_t *len) +{ + /* translate the mode */ + const char *str_mode = "ascii"; + + if (mode == TTM_OCTET) { + str_mode = "bin"; + } + else if (mode == TTM_MAIL) { + str_mode = "mail"; + } + + /* translate the action */ + const char *str_action = "read"; + if (action == TFTP_WRITE) { + str_action = "write"; + } + + /* display the action being performed */ + printf("tftp_client: %s %s %s:%u\n", str_mode, str_action, file_name, *len); + + /* return the length of the text, if this is an read action */ + if (action == TFTP_READ) { + *len = sizeof(_tftp_client_hello); + } + + /* remember the action of the current transfer */ + _tftp_action = action; + + /* we accept the transfer to take place so we return true */ + return true; +} + +/** + * @brief called to get or put data, depending on the mode received by `_tftp_start_cb(action, ...)` + */ +static int _tftp_client_data_cb(uint32_t offset, void *data, size_t data_len) +{ + char *c = (char *) data; + + /* if we are reading return the part of the data that is being requested */ + if (_tftp_action == TFTP_WRITE) { + /* calculate the length of the data block to transfer */ + if (offset + data_len > sizeof(_tftp_client_hello)) { + data_len -= (offset + data_len) - sizeof(_tftp_client_hello); + } + + /* copy the block to the output buffer */ + memcpy(data, _tftp_client_hello + offset, data_len); + } + else { + /* we received a data block which we output to the console */ + printf("\n -- CLIENT DATA --\n%.*s\n -- CLIENT DATA --\n", data_len, c); + } + + /* return the length of the data block */ + return data_len; +} + +/** + * @brief the transfer has stopped, see the event argument to determined if it was successful + * or not. + */ +static void _tftp_client_stop_cb(tftp_event_t event, const char *msg) +{ + /* decode the stop event received */ + const char *cause = "UNKOWN"; + + if (event == TFTP_SUCCESS) { + cause = "SUCCESS"; + } + else if (event == TFTP_PEER_ERROR) { + cause = "ERROR From Client"; + } + else if (event == TFTP_INTERN_ERROR) { + cause = "ERROR Internal Server Error"; + } + + /* print the transfer result to the console */ + printf("tftp_client: %s: %s\n", cause, msg); +} + +static int _tftp_client_cmd(int argc, char * *argv) +{ + ipv6_addr_t ip; + const char *file_name = argv[2]; + tftp_mode_t mode = TTM_OCTET; + bool use_options = true; + + ipv6_addr_from_str(&ip, _tftp_default_host); + + if (argc >= 3 && argc <= 6) { + /* decode the action */ + if (strcmp(argv[1], "get") == 0) { + _tftp_action = TFTP_READ; + } + else if (strcmp(argv[1], "put") == 0) { + _tftp_action = TFTP_WRITE; + } + else { + return -1; + } + + /* get the transfer mode */ + if (argc >= 4) { + if (strcmp(argv[3], "octet") == 0) { + mode = TTM_OCTET; + } + else if (strcmp(argv[3], "ascii") == 0) { + mode = TTM_ASCII; + } + else if (strcmp(argv[3], "mail") == 0) { + mode = TTM_MAIL; + } + else { + puts("tftp: couldn't parse the TFTP transfer mode"); + return -1; + } + } + + /* decode if we must use the TFTP option extension or not */ + if (argc >= 5) { + if (strcmp(argv[4], "0") == 0) { + use_options = false; + } + else if (strcmp(argv[4], "1") == 0) { + use_options = true; + } + else { + puts("tftp: invalid options choose 0 or 1"); + return -1; + } + } + + /* decode the address */ + if (argc >= 6) { + if (!ipv6_addr_from_str(&ip, argv[5])) { + puts("tftp: invalid IP address"); + return -1; + } + } + } + else { + return -1; + } + + if (_tftp_action == TFTP_READ) { + puts("tftp: starting read request"); + + gnrc_tftp_client_read(&ip, file_name, mode, _tftp_client_data_cb, + _tftp_client_start_cb, _tftp_client_stop_cb, use_options); + } + else if (_tftp_action == TFTP_WRITE) { + puts("tftp: starting write request"); + + gnrc_tftp_client_write(&ip, file_name, mode, _tftp_client_data_cb, + sizeof(_tftp_client_hello), _tftp_client_stop_cb, use_options); + } + + return 0; +} + +/** + * @brief start the TFTP server by creating a thread + */ +int tftp_client_cmd(int argc, char * *argv) +{ + if (_tftp_client_cmd(argc, argv) < 0) { + printf("usage: %s <action[get|put]> <file_name> <mode[ascii | octet(default) | mail]>\n" + "\t<use_options[1 (default) | 0]> <addr(default: ::1)>\n", argv[0]); + } + + return 0; +} diff --git a/examples/gnrc_tftp/tftp_server.c b/examples/gnrc_tftp/tftp_server.c new file mode 100644 index 0000000000000000000000000000000000000000..b825b0831df950def595464b7f5ea1caaf3820fd --- /dev/null +++ b/examples/gnrc_tftp/tftp_server.c @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2015 Engineering-Spirit + * + * 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 examples + * @{ + * + * @file + * @brief Demonstrating the sending and receiving of data via the TFTP server + * + * @author Nick van IJzendoorn <nijzendoorn@engineering-spirit.nl> + * + * @} + */ + +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <inttypes.h> + +#include "thread.h" +#include "net/gnrc/tftp.h" + +/* the message queues */ +#define TFTP_QUEUE_SIZE (4) +static msg_t _tftp_msg_queue[TFTP_QUEUE_SIZE]; + +/* allocate the stack */ +char _tftp_stack[THREAD_STACKSIZE_MAIN + THREAD_EXTRA_STACKSIZE_PRINTF]; + +/* default server text which can be received */ +static const char _tftp_server_hello[] = "Hello world,\n" + "\n" + "Welcome to the wonderful world of IoT and embedded systems.\n" + "\n" + "Enjoy the RIOT-OS\n"; + +static tftp_action_t _tftp_action; + +/** + * @brief called at every transcation start + */ +static bool _tftp_server_start_cb(tftp_action_t action, tftp_mode_t mode, + const char *file_name, size_t *len) +{ + /* translate the mode */ + const char *str_mode = "ascii"; + + if (mode == TTM_OCTET) { + str_mode = "bin"; + } + else if (mode == TTM_MAIL) { + str_mode = "mail"; + } + + /* translate the action */ + const char *str_action = "read"; + if (action == TFTP_WRITE) { + str_action = "write"; + } + + /* display the action being performed */ + printf("tftp_server: %s %s %s:%u\n", str_mode, str_action, file_name, *len); + + /* return the length of the text, if this is an read action */ + if (action == TFTP_READ) { + *len = sizeof(_tftp_server_hello); + } + + /* remember the action of the current transfer */ + _tftp_action = action; + + /* we accept the transfer to take place so we return true */ + return true; +} + +/** + * @brief called to get or put data, depending on the mode received by `_tftp_start_cb(action, ...)` + */ +static int _tftp_server_data_cb(uint32_t offset, void *data, size_t data_len) +{ + char *c = (char *) data; + + /* if we are reading return the part of the data that is being requested */ + if (_tftp_action == TFTP_READ) { + /* calculate the length of the data block to transfer */ + if (offset + data_len > sizeof(_tftp_server_hello)) { + data_len -= (offset + data_len) - sizeof(_tftp_server_hello); + } + + /* copy the block to the output buffer */ + memcpy(data, _tftp_server_hello + offset, data_len); + } + else { + /* we received a data block which we output to the console */ + printf("\n -- SERVER DATA --\n%.*s\n -- SERVER DATA --\n", data_len, c); + } + + /* return the length of the data block */ + return data_len; +} + +/** + * @brief the transfer has stopped, see the event argument to determined if it was successful + * or not. + */ +static void _tftp_server_stop_cb(tftp_event_t event, const char *msg) +{ + /* decode the stop event received */ + const char *cause = "UNKOWN"; + + if (event == TFTP_SUCCESS) { + cause = "SUCCESS"; + } + else if (event == TFTP_PEER_ERROR) { + cause = "ERROR From Client"; + } + else if (event == TFTP_INTERN_ERROR) { + cause = "ERROR Internal Server Error"; + } + + /* print the transfer result to the console */ + printf("tftp_server: %s: %s\n", cause, (msg == NULL) ? "NULL" : msg); +} + +/** + * @brief the TFTP server thread + */ +void *tftp_server_wrapper(void *arg) +{ + (void)arg; + + /* A message queue is needed to register for incoming packets */ + msg_init_queue(_tftp_msg_queue, TFTP_QUEUE_SIZE); + + /* inform the user */ + puts("tftp_server: Starting TFTP service at port 69"); + + /* run the TFTP server */ + gnrc_tftp_server(_tftp_server_data_cb, _tftp_server_start_cb, _tftp_server_stop_cb, true); + + /* the TFTP server has been stopped */ + puts("tftp_server: Stopped TFTP service"); + + return NULL; +} + +/** + * @brief start the TFTP server by creating a thread + */ +void tftp_server_start(void) +{ + thread_create(_tftp_stack, sizeof(_tftp_stack), + 1, CREATE_WOUT_YIELD | CREATE_STACKTEST, + tftp_server_wrapper, NULL, "TFTP Server"); +} + +/** + * @brief stop the TFTP server by sending a message to the thread + */ +void tftp_server_stop(void) +{ + gnrc_tftp_server_stop(); +} + +int tftp_server_cmd(int argc, char * *argv) +{ + switch (argc) { + case 2: + if (strcmp(argv[1], "start") == 0) { + tftp_server_start(); + return 0; + } + else if (strcmp(argv[1], "stop") == 0) { + tftp_server_stop(); + return 0; + } + /* no break */ + + default: + printf("usage: %s [start|stop]\n", argv[0]); + return 0; + } + + return 0; +} diff --git a/sys/include/net/gnrc/tftp.h b/sys/include/net/gnrc/tftp.h new file mode 100644 index 0000000000000000000000000000000000000000..13337e1a29f123471f9412eb9d72f4f7f7a75487 --- /dev/null +++ b/sys/include/net/gnrc/tftp.h @@ -0,0 +1,206 @@ +/* + * 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. + */ + +/** + * @defgroup net_gnrc_tftp TFTP Support Library + * @ingroup net_gnrc + * @brief Add's support for TFTP protocol parsing + * @{ + * + * @file + * @brief TFTP support library + * + * The TFTP module add's support for the TFTP protocol. + * It implements the following RFC's: + * - https://tools.ietf.org/html/rfc1350 + * (RFC1350 The TFTP Protocol (Revision 2) + * + * - https://tools.ietf.org/html/rfc2347 + * (RFC2347 TFTP Option Extension) + * + * - https://tools.ietf.org/html/rfc2348 + * (RFC2348 TFTP Blocksize Option) + * + * - https://tools.ietf.org/html/rfc2349 + * (RFC2349 TFTP Timeout Interval and Transfer Size Options) + * + * @author Nick van IJzendoorn <nijzendoorn@engineering-spirit.nl> + */ + +#ifndef GNRC_TFTP_H_ +#define GNRC_TFTP_H_ + +#include <inttypes.h> + +#include "byteorder.h" +#include "kernel_types.h" +#include "net/ipv6/addr.h" +#include "net/gnrc/nettype.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief The maximum allowed length of the transfer filename + */ +#ifndef GNRC_TFTP_MAX_FILENAME_LEN +#define GNRC_TFTP_MAX_FILENAME_LEN (64) +#endif + +/** + * @brief The base source port to be used by TFTP + */ +#ifndef GNRC_TFTP_DEFAULT_SRC_PORT +#define GNRC_TFTP_DEFAULT_SRC_PORT (10690) +#endif + +/** + * @brief The default destination port of the TFTP server + */ +#ifndef GNRC_TFTP_DEFAULT_DST_PORT +#define GNRC_TFTP_DEFAULT_DST_PORT (69) +#endif + +/** + * @brief The maximum allowed data bytes in the data packet + */ +#ifndef GNRC_TFTP_MAX_TRANSFER_UNIT +#define GNRC_TFTP_MAX_TRANSFER_UNIT (512) +#endif + +/** + * @brief The number of retries that must be made before stopping a transfer + */ +#ifndef GNRC_TFTP_MAX_RETRIES +#define GNRC_TFTP_MAX_RETRIES (5) +#endif + +/** + * @brief The default timeout of a data packet + */ +#ifndef GNRC_TFTP_DEFAULT_TIMEOUT +#define GNRC_TFTP_DEFAULT_TIMEOUT (1 * SEC_IN_USEC) +#endif + +/** + * @brief TFTP action to perform + */ +typedef enum { + TFTP_READ, + TFTP_WRITE +} tftp_action_t; + +/** + * @brief TFTP Transfer modes + */ +typedef enum { + TTM_ASCII, + TTM_OCTET, + TTM_MAIL +} tftp_mode_t; + +/** + * @brief TFTP stop / finish events + */ +typedef enum { + TFTP_SUCCESS, /**< The transfer was successful */ + TFTP_PEER_ERROR, /**< The peer send the given error */ + TFTP_INTERN_ERROR /**< There was an internal error */ +} tftp_event_t; + +/** + * @brief callback define which is called when a new server request is placed + * or when an client read request is made and the data length option is received + * + * @param [in] action The action the transfer want to perform + * @param [in] mode The data mode of the transfer + * @param [in] file_name The filename of the file being transfered + * @param [in/out] data_len When a read action is performed, the application must give + * the total transfer size of the data. When a write action + * is performed the total transfer size will be given. + */ +typedef bool (*tftp_start_cb_t)(tftp_action_t action, tftp_mode_t mode, + const char *file_name, size_t *data_len); + +/** + * @brief callback define which is called to get or set data from/to the user application + */ +typedef int (*tftp_data_cb_t)(uint32_t offset, void *data, size_t data_len); + +/** + * @brief callback define which is called when an transfer is stopped + */ +typedef void (*tftp_stop_cb_t)(tftp_event_t event, const char *msg); + +/** + * @brief Start the TFTP server + * + * @param [in] data_cb called for each read data block + * @param [in] start_cb called if a new client connection is requested + * @param [in] stop_cb called if the transfer has finished + * @param [in] use_options when set the client uses the option extensions + * + * @return 1 on success + * @return -1 on failure + */ +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); + +/** + * @brief Stop the TFTP server + * + * @return 1 on success + * @return -1 on failure + */ +int gnrc_tftp_server_stop(void); + +/** + * @brief Start an TFTP client read action from the given destination + * + * @param [in] addr the address of the server + * @param [in] file_name the filename of the file to get + * @param [in] mode the transfer mode + * @param [in] data_cb called for each read data block + * @param [in] start_cb called if the server returns the transfer_size option + * @param [in] stop_cb called if the transfer has finished + * @param [in] use_option when set the client uses the option extensions + * + * @return 1 on success + * @return -1 on failure + */ +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); + +/** + * @brief Start an TFTP client write action to the given destination + * + * @param [in] addr the address of the server + * @param [in] file_name the filename of the file to write + * @param [in] mode the transfer mode + * @param [in] data_cb called to store the received block + * @param [in] total_size the total size of the transfer + * @param [in] stop_cb called if the server returns the transfer_size option + * @param [in] use_option when set the client uses the option extensions + * + * @return 1 on success + * @return -1 on failure + */ +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); + +#ifdef __cplusplus +} +#endif + +#endif /* GNRC_TFTP_H_ */ + +/** + * @} + */ diff --git a/sys/net/gnrc/Makefile b/sys/net/gnrc/Makefile index 6ac2001c7a705084ec9aa167c0242e691b0d1512..54fe9e810768a3e88145264354e3382342c50d67 100644 --- a/sys/net/gnrc/Makefile +++ b/sys/net/gnrc/Makefile @@ -109,5 +109,8 @@ endif ifneq (,$(filter gnrc_zep,$(USEMODULE))) DIRS += application_layer/zep endif +ifneq (,$(filter gnrc_tftp,$(USEMODULE))) + DIRS += application_layer/tftp +endif include $(RIOTBASE)/Makefile.base diff --git a/sys/net/gnrc/application_layer/tftp/Makefile b/sys/net/gnrc/application_layer/tftp/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..4232bae24fff702f4636b64d6488ddabb0a0b2dd --- /dev/null +++ b/sys/net/gnrc/application_layer/tftp/Makefile @@ -0,0 +1,3 @@ +MODULE = gnrc_tftp + +include $(RIOTBASE)/Makefile.base diff --git a/sys/net/gnrc/application_layer/tftp/gnrc_tftp.c b/sys/net/gnrc/application_layer/tftp/gnrc_tftp.c new file mode 100644 index 0000000000000000000000000000000000000000..30bc583b8a7379e49e00e9428c1452a9f0fb30db --- /dev/null +++ b/sys/net/gnrc/application_layer/tftp/gnrc_tftp.c @@ -0,0 +1,1139 @@ +/* + * 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 = (genrand_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 = { NULL, GNRC_TFTP_DEFAULT_DST_PORT, + thread_getpid() }; + + 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."); + 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((gnrc_pktsnip_t *) 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 = { NULL, ctxt->src_port, thread_getpid() }; + + if (gnrc_netreg_register(GNRC_NETTYPE_UDP, &entry)) { + DEBUG("tftp: error starting server."); + 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((gnrc_pktsnip_t *) 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"); + gnrc_pktbuf_release(outbuf); + return TS_BUSY; + } + + gnrc_pktsnip_t *pkt = (gnrc_pktsnip_t *)(m->content.ptr); + + gnrc_pktsnip_t *tmp; + LL_SEARCH_SCALAR(pkt, tmp, type, GNRC_NETTYPE_UDP); + udp_hdr_t *udp = (udp_hdr_t *)tmp->data; + + LL_SEARCH_SCALAR(pkt, tmp, type, 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 */ + ctxt->entry.next = NULL; + ctxt->entry.demux_ctx = ctxt->src_port; + ctxt->entry.pid = thread_getpid(); + 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"); + } + 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 / SEC_IN_USEC)); + + /** + * 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"); + 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"); + 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.u8, sizeof(src_port), + dst_port.u8, sizeof(dst_port)); + if (udp == NULL) { + DEBUG("tftp: error unable to allocate UDP header"); + 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, 0, ctxt->peer.u8, sizeof(ipv6_addr_t)); + if (ip == NULL) { + DEBUG("tftp: error unable to allocate IPv6 header"); + 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"); + 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 / MS_IN_USEC); + } + + 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 = %d\n", buf->size); + while ((offset + sizeof(uint16_t)) < (buf->size)) { + DEBUG("tftp: offset = %d\n", 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) * SEC_IN_USEC; + DEBUG("tftp: option TOPT_TIMEOUT = %" PRIu32 " ms\n", ctxt->timeout / MS_IN_USEC); + break; + } + + break; + } + } + } + + DEBUG("tftp: return %d\n", 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; +} + +/** + * @} + */