| // Copyright 2017 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #define _POSIX_C_SOURCE 200809L // for strnlen |
| #include <arpa/inet.h> |
| #include <ctype.h> |
| #include <inttypes.h> |
| #include <stdarg.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <strings.h> |
| |
| #include "internal.h" |
| |
| // TODO: update this |
| // RRQ -> |
| // <- DATA or OACK or ERROR |
| // ACK(0) -> (to confirm reception of OACK) |
| // ERROR -> (on OACK with non requested options) |
| // <- DATA(1) |
| // ACK(1) -> |
| |
| // WRQ -> |
| // <- ACK or OACK or ERROR |
| // DATA(1) -> |
| // ERROR -> (on OACK with non requested options) |
| // <- DATA(2) |
| // ACK(2) -> |
| |
| // MODE |
| static const char* kNetascii = "NETASCII"; |
| static const char* kOctet = "OCTET"; |
| static const char* kMail = "MAIL"; |
| static const size_t kMaxMode = 9; // strlen(NETASCII) + 1 |
| |
| // TSIZE |
| // Limit transfer to less than 10GB |
| static const char* kTsize = "TSIZE"; |
| static const size_t kTsizeLen = 5; // strlen(kTsize) |
| static const size_t kMaxTsizeOpt = 17; // strlen(TSIZE) + 1 + strlen(1000000000) + 1 |
| |
| // BLKSIZE |
| // Max size is 65535 (max IP datagram) |
| static const char* kBlkSize = "BLKSIZE"; |
| static const size_t kBlkSizeLen = 7; // strlen(kBlkSize) |
| static const size_t kMaxBlkSizeOpt = 15; // kBlkSizeLen + strlen("!") + 1 + strlen(65535) + 1 |
| |
| // TIMEOUT |
| // Max is 255 (RFC 2349) |
| static const char* kTimeout = "TIMEOUT"; |
| static const size_t kTimeoutLen = 7; // strlen(kTimeout) |
| static const size_t kMaxTimeoutOpt = 13; // kTimeoutLen + strlen("!") + 1 + strlen(255) + 1; |
| |
| // WINDOWSIZE |
| // Max is 65535 (RFC 7440) |
| static const char* kWindowSize = "WINDOWSIZE"; |
| static const size_t kWindowSizeLen = 10; // strlen(kWindowSize); |
| static const size_t kMaxWindowSizeOpt = |
| 18; // kWindowSizeLen + strlen("!") + 1 + strlen(65535) + 1; |
| |
| // Since RRQ and WRQ come before option negotation, they are limited to max TFTP |
| // blocksize of 512 (RFC 1350 and 2347). |
| static const size_t kMaxRequestSize = 512; |
| |
| #if defined(TFTP_HOSTLIB) |
| // Host (e.g., netcp, bootserver) |
| #include <time.h> |
| #define DEBUG 0 |
| #elif defined(TFTP_USERLIB) |
| // Fuchsia (e.g., netsvc) |
| #define DEBUG 0 |
| #elif defined(TFTP_EFILIB) |
| // Bootloader: use judiciously, since the console can easily become overwhelmed and hang |
| #define DEBUG 0 |
| #else |
| #error unable to identify target environment |
| #endif |
| |
| #if DEBUG |
| #define xprintf(args...) fprintf(stderr, args) |
| #else |
| #define xprintf(args...) |
| #endif |
| |
| #define __ATTR_PRINTF(__fmt, __varargs) __attribute__((__format__(__printf__, __fmt, __varargs))) |
| #define MIN(x, y) ((x) < (y) ? (x) : (y)) |
| |
| static void append_option_name(char** body, size_t* left, const char* name) { |
| size_t offset = strlen(name); |
| memcpy(*body, name, offset); |
| offset++; |
| *body += offset; |
| *left -= offset; |
| } |
| |
| static void __ATTR_PRINTF(5, 6) |
| append_option(char** body, size_t* left, const char* name, bool force, const char* fmt, ...) { |
| char* bodyp = *body; |
| size_t leftp = *left; |
| |
| size_t offset = strlen(name); |
| memcpy(bodyp, name, offset); |
| if (force) { |
| bodyp[offset] = '!'; |
| offset++; |
| } |
| offset++; |
| bodyp += offset; |
| leftp -= offset; |
| va_list args; |
| va_start(args, fmt); |
| offset = vsnprintf(bodyp, leftp - 1, fmt, args); |
| va_end(args); |
| offset++; |
| bodyp += offset; |
| leftp -= offset; |
| |
| *body = bodyp; |
| *left = leftp; |
| } |
| |
| #define OPCODE(session, msg, value) \ |
| do { \ |
| if (session->use_opcode_prefix) { \ |
| (msg)->opcode = htons((value & 0xff) | ((uint16_t)session->opcode_prefix << 8)); \ |
| } else { \ |
| (msg)->opcode = htons(value); \ |
| } \ |
| } while (0) |
| |
| #define TRANSMIT_MORE 1 |
| #define TRANSMIT_WAIT_ON_ACK 2 |
| |
| static size_t next_option(char* buffer, size_t len, char** option, char** value) { |
| size_t left = len; |
| size_t option_len = strnlen(buffer, left); |
| if (option_len == len) { |
| return 0; |
| } |
| |
| *option = buffer; |
| xprintf("'%s' %ld\n", *option, option_len); |
| buffer += option_len + 1; |
| left -= option_len + 1; |
| size_t value_len = strnlen(buffer, left); |
| if (value_len == left) { |
| return 0; |
| } |
| *value = buffer; |
| xprintf("'%s' %ld\n", *value, value_len); |
| left -= value_len + 1; |
| return len - left; |
| } |
| |
| /* Build an err packet in resp_buf and set session state to ERROR |
| |
| 2 bytes 2 bytes string 1 byte |
| +--------------+----------+---------+------+ |
| | OPCODE_ERROR | ERR_CODE | ERR_MSG | 0 | |
| +--------------+----------+---------+------+ |
| */ |
| static void set_error(tftp_session* session, uint16_t err_code, void* resp_buf, size_t* resp_len, |
| const char* err_msg) { |
| tftp_err_msg* resp = resp_buf; |
| OPCODE(session, resp, OPCODE_ERROR); |
| resp->err_code = htons(err_code); |
| size_t err_msg_len = strlen(err_msg); |
| size_t max_msg_sz = *resp_len - (sizeof(tftp_err_msg) + 1); |
| if (err_msg_len >= max_msg_sz) { |
| memcpy(resp->msg, err_msg, max_msg_sz); |
| resp->msg[max_msg_sz] = '\0'; |
| // *resp_len is unchanged - the whole buffer was used |
| } else { |
| strcpy(resp->msg, err_msg); |
| *resp_len = sizeof(tftp_err_msg) + err_msg_len + 1; |
| } |
| session->state = ERROR; |
| } |
| |
| tftp_status tx_data(tftp_session* session, tftp_data_msg* resp, size_t* outlen, void* cookie) { |
| session->offset = (session->block_number + session->window_index) * session->block_size; |
| *outlen = 0; |
| if (session->offset <= session->file_size) { |
| session->window_index++; |
| OPCODE(session, resp, OPCODE_DATA); |
| resp->block = htons(session->block_number + session->window_index); |
| size_t len = MIN(session->file_size - session->offset, session->block_size); |
| xprintf(" -> Copying block #%" PRIu64 " (size:%zu/%d) from %zu/%zu [%d/%d]\n", |
| session->block_number + session->window_index, len, session->block_size, |
| session->offset, session->file_size, session->window_index, session->window_size); |
| void* buf = resp->data; |
| size_t len_remaining = len; |
| size_t off = session->offset; |
| while (len_remaining > 0) { |
| // TODO(tkilbourn): assert that these function pointers are set |
| size_t rr = len_remaining; |
| tftp_status s = session->file_interface.read(buf, &rr, off, cookie); |
| if (s < 0) { |
| xprintf("Err reading: %d\n", s); |
| return s; |
| } |
| buf += rr; |
| off += rr; |
| len_remaining -= rr; |
| } |
| *outlen = sizeof(*resp) + len; |
| |
| if (session->window_index < session->window_size) { |
| xprintf(" -> TRANSMIT_MORE(%d < %d)\n", session->window_index, session->window_size); |
| } else { |
| xprintf(" -> TRANSMIT_WAIT_ON_ACK(%d >= %d)\n", session->window_index, session->window_size); |
| } |
| } else { |
| xprintf(" -> TRANSMIT_WAIT_ON_ACK(completed)\n"); |
| } |
| return TFTP_NO_ERROR; |
| } |
| |
| size_t tftp_sizeof_session(void) { return sizeof(tftp_session); } |
| |
| tftp_status tftp_init(tftp_session** session, void* buffer, size_t size) { |
| if (buffer == NULL) { |
| return TFTP_ERR_INVALID_ARGS; |
| } |
| if (size < sizeof(tftp_session)) { |
| return TFTP_ERR_BUFFER_TOO_SMALL; |
| } |
| *session = buffer; |
| tftp_session* s = *session; |
| memset(s, 0, sizeof(tftp_session)); |
| |
| // Sensible defaults for non-negotiated values |
| s->file_size = DEFAULT_FILESIZE; |
| s->mode = DEFAULT_MODE; |
| s->max_timeouts = DEFAULT_MAX_TIMEOUTS; |
| s->use_opcode_prefix = DEFAULT_USE_OPCODE_PREFIX; |
| |
| return TFTP_NO_ERROR; |
| } |
| |
| tftp_status tftp_session_set_file_interface(tftp_session* session, tftp_file_interface* callbacks) { |
| if (session == NULL) { |
| return TFTP_ERR_INVALID_ARGS; |
| } |
| |
| session->file_interface = *callbacks; |
| return TFTP_NO_ERROR; |
| } |
| |
| tftp_status tftp_session_set_transport_interface(tftp_session* session, |
| tftp_transport_interface* callbacks) { |
| if (session == NULL) { |
| return TFTP_ERR_INVALID_ARGS; |
| } |
| session->transport_interface = *callbacks; |
| return TFTP_NO_ERROR; |
| } |
| |
| bool tftp_session_has_pending(tftp_session* session) { |
| return session->direction == SEND_FILE && session->window_index > 0 && |
| session->window_index < session->window_size && |
| ((session->block_number + session->window_index) * session->block_size) <= |
| session->file_size; |
| } |
| |
| tftp_status tftp_set_options(tftp_session* session, const uint16_t* block_size, |
| const uint8_t* timeout, const uint16_t* window_size) { |
| session->options.mask = 0; |
| if (block_size) { |
| session->options.block_size = *block_size; |
| session->options.mask |= BLOCKSIZE_OPTION; |
| } |
| if (timeout) { |
| session->options.timeout = *timeout; |
| session->options.mask |= TIMEOUT_OPTION; |
| } |
| if (window_size) { |
| session->options.window_size = *window_size; |
| session->options.mask |= WINDOWSIZE_OPTION; |
| } |
| return TFTP_NO_ERROR; |
| } |
| |
| tftp_status tftp_generate_request(tftp_session* session, tftp_file_direction direction, |
| const char* local_filename, const char* remote_filename, |
| tftp_mode mode, size_t datalen, const uint16_t* block_size, |
| const uint8_t* timeout, const uint16_t* window_size, |
| void* outgoing, size_t* outlen, uint32_t* timeout_ms) { |
| if (*outlen < 2) { |
| xprintf("outlen too short: %zd\n", *outlen); |
| return TFTP_ERR_BUFFER_TOO_SMALL; |
| } |
| |
| // The actual options are not set until we get a confirmation OACK message. Until then, |
| // we have to assume the TFTP defaults. |
| session->block_size = DEFAULT_BLOCKSIZE; |
| session->timeout = DEFAULT_TIMEOUT; |
| session->window_size = DEFAULT_WINDOWSIZE; |
| |
| tftp_msg* ack = outgoing; |
| OPCODE(session, ack, (direction == SEND_FILE) ? OPCODE_WRQ : OPCODE_RRQ); |
| char* body = ack->data; |
| memset(body, 0, *outlen - sizeof(*ack)); |
| size_t left = *outlen - sizeof(*ack); |
| size_t remote_filename_len = strlen(remote_filename); |
| if (remote_filename_len + 1 > left - kMaxMode) { |
| xprintf("filename too long %zd > %zd\n", remote_filename_len, left - kMaxMode); |
| return TFTP_ERR_INVALID_ARGS; |
| } |
| memcpy(body, remote_filename, remote_filename_len); |
| body += remote_filename_len + 1; |
| left -= remote_filename_len + 1; |
| strncpy(session->filename, local_filename, sizeof(session->filename) - 1); |
| session->filename[sizeof(session->filename) - 1] = '\0'; |
| switch (mode) { |
| case MODE_NETASCII: |
| append_option_name(&body, &left, kNetascii); |
| break; |
| case MODE_OCTET: |
| append_option_name(&body, &left, kOctet); |
| break; |
| case MODE_MAIL: |
| append_option_name(&body, &left, kMail); |
| break; |
| default: |
| return TFTP_ERR_INVALID_ARGS; |
| } |
| session->mode = mode; |
| |
| if (left < kMaxTsizeOpt) { |
| return TFTP_ERR_BUFFER_TOO_SMALL; |
| } |
| append_option(&body, &left, kTsize, false, "%zu", datalen); |
| session->file_size = datalen; |
| tftp_options* sent_opts = &session->client_sent_opts; |
| sent_opts->mask = 0; |
| |
| if (block_size || session->options.mask & BLOCKSIZE_OPTION) { |
| if (left < kMaxBlkSizeOpt) { |
| return TFTP_ERR_BUFFER_TOO_SMALL; |
| } |
| bool force_value; |
| if (block_size) { |
| force_value = true; |
| sent_opts->block_size = *block_size; |
| } else { |
| force_value = false; |
| sent_opts->block_size = session->options.block_size; |
| } |
| append_option(&body, &left, kBlkSize, force_value, "%" PRIu16, sent_opts->block_size); |
| sent_opts->mask |= BLOCKSIZE_OPTION; |
| } |
| |
| if (timeout || session->options.mask & TIMEOUT_OPTION) { |
| if (left < kMaxTimeoutOpt) { |
| return TFTP_ERR_BUFFER_TOO_SMALL; |
| } |
| bool force_value; |
| if (timeout) { |
| force_value = true; |
| sent_opts->timeout = *timeout; |
| } else { |
| force_value = false; |
| sent_opts->timeout = session->options.timeout; |
| } |
| append_option(&body, &left, kTimeout, force_value, "%" PRIu8, sent_opts->timeout); |
| sent_opts->mask |= TIMEOUT_OPTION; |
| } |
| |
| if (window_size || session->options.mask & WINDOWSIZE_OPTION) { |
| if (left < kMaxWindowSizeOpt) { |
| return TFTP_ERR_BUFFER_TOO_SMALL; |
| } |
| bool force_value; |
| if (window_size) { |
| force_value = true; |
| sent_opts->window_size = *window_size; |
| } else { |
| force_value = false; |
| sent_opts->window_size = session->options.window_size; |
| } |
| append_option(&body, &left, kWindowSize, force_value, "%" PRIu16, sent_opts->window_size); |
| sent_opts->mask |= WINDOWSIZE_OPTION; |
| } |
| |
| *outlen = *outlen - left; |
| // Nothing has been negotiated yet so use default |
| *timeout_ms = 1000 * session->timeout; |
| |
| session->direction = direction; |
| session->state = REQ_SENT; |
| xprintf("Generated %s request, len=%zu\n", (direction == SEND_FILE) ? "write" : "read", *outlen); |
| return TFTP_NO_ERROR; |
| } |
| |
| tftp_status tftp_handle_request(tftp_session* session, tftp_file_direction direction, tftp_msg* req, |
| size_t req_len, tftp_msg* resp, size_t* resp_len, |
| uint32_t* timeout_ms, void* cookie) { |
| // We could be in REQ_RECEIVED if our OACK was dropped. |
| if (session->state != NONE && session->state != REQ_RECEIVED) { |
| xprintf("Invalid state transition %d -> %d\n", session->state, REQ_RECEIVED); |
| set_error(session, TFTP_ERR_CODE_UNDEF, resp, resp_len, "invalid state transition"); |
| return TFTP_ERR_BAD_STATE; |
| } |
| // opcode, filename, 0, mode, 0, opt1, 0, value1 ... optN, 0, valueN, 0 |
| // Max length is 512 no matter |
| if (req_len > kMaxRequestSize) { |
| xprintf("Write request is too large\n"); |
| set_error(session, TFTP_ERR_CODE_UNDEF, resp, resp_len, "write request is too large"); |
| return TFTP_ERR_INTERNAL; |
| } |
| // Skip opcode |
| size_t left = req_len - sizeof(*resp); |
| char* cur = req->data; |
| char *option, *value; |
| // filename, 0, mode, 0 can be interpreted like option, 0, value, 0 |
| size_t offset = next_option(cur, left, &option, &value); |
| if (!offset) { |
| xprintf("No options\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "no options"); |
| return TFTP_ERR_INTERNAL; |
| } |
| left -= offset; |
| |
| xprintf("filename = '%s', mode = '%s'\n", option, value); |
| |
| strncpy(session->filename, option, sizeof(session->filename) - 1); |
| session->filename[sizeof(session->filename) - 1] = '\0'; |
| char* mode = value; |
| if (!strncasecmp(mode, kNetascii, strlen(kNetascii))) { |
| session->mode = MODE_NETASCII; |
| } else if (!strncasecmp(mode, kOctet, strlen(kOctet))) { |
| session->mode = MODE_OCTET; |
| } else if (!strncasecmp(mode, kMail, strlen(kMail))) { |
| session->mode = MODE_MAIL; |
| } else { |
| xprintf("Unknown write request mode\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "unknown write request mode"); |
| return TFTP_ERR_INTERNAL; |
| } |
| |
| // Initialize the values to TFTP defaults |
| session->block_size = DEFAULT_BLOCKSIZE; |
| session->timeout = DEFAULT_TIMEOUT; |
| session->window_size = DEFAULT_WINDOWSIZE; |
| |
| // TODO(tkilbourn): refactor option handling code to share with |
| // tftp_handle_oack |
| cur += offset; |
| bool file_size_seen = false; |
| tftp_options requested_options = {.mask = 0}; |
| tftp_options* override_opts = &session->options; |
| while (offset > 0 && left > 0) { |
| offset = next_option(cur, left, &option, &value); |
| if (!offset) { |
| xprintf("No more options\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "no more options"); |
| return TFTP_ERR_INTERNAL; |
| } |
| |
| if (!strncasecmp(option, kTsize, kTsizeLen)) { // RFC 2349 |
| if (direction == RECV_FILE) { |
| long val = atol(value); |
| if (val < 0) { |
| xprintf("invalid file size\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "invalid file size"); |
| return TFTP_ERR_INTERNAL; |
| } |
| session->file_size = val; |
| } |
| file_size_seen = true; |
| } else if (!strncasecmp(option, kBlkSize, kBlkSizeLen)) { // RFC 2348 |
| bool force_block_size = (option[kBlkSizeLen] == '!'); |
| // Valid values range between "8" and "65464" octets, inclusive |
| long val = atol(value); |
| // TODO(tkilbourn): with an MTU of 1500, shouldn't be more than 1428 |
| if (val < 8 || val > 65464) { |
| xprintf("invalid block size\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "invalid block size"); |
| return TFTP_ERR_INTERNAL; |
| } |
| requested_options.block_size = val; |
| requested_options.mask |= BLOCKSIZE_OPTION; |
| if (force_block_size || !(override_opts->mask & BLOCKSIZE_OPTION)) { |
| session->block_size = val; |
| } else { |
| session->block_size = override_opts->block_size; |
| } |
| } else if (!strncasecmp(option, kTimeout, kTimeoutLen)) { // RFC 2349 |
| bool force_timeout_val = (option[kTimeoutLen] == '!'); |
| // Valid values range between "1" and "255" seconds inclusive. |
| long val = atol(value); |
| if (val < 1 || val > 255) { |
| xprintf("invalid timeout\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "invalid timeout"); |
| return TFTP_ERR_INTERNAL; |
| } |
| requested_options.timeout = val; |
| requested_options.mask |= TIMEOUT_OPTION; |
| if (force_timeout_val || !(override_opts->mask & TIMEOUT_OPTION)) { |
| session->timeout = val; |
| } else { |
| session->timeout = override_opts->timeout; |
| } |
| } else if (!strncasecmp(option, kWindowSize, kWindowSizeLen)) { // RFC 7440 |
| bool force_window_size = (option[kWindowSizeLen] == '!'); |
| // The valid values range MUST be between 1 and 65535 blocks, inclusive. |
| long val = atol(value); |
| if (val < 1 || val > 65535) { |
| xprintf("invalid window size\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "invalid window size"); |
| return TFTP_ERR_INTERNAL; |
| } |
| requested_options.window_size = val; |
| requested_options.mask |= WINDOWSIZE_OPTION; |
| if (force_window_size || !(override_opts->mask & WINDOWSIZE_OPTION)) { |
| session->window_size = val; |
| } else { |
| session->window_size = override_opts->window_size; |
| } |
| } else { |
| // Options which the server does not support should be omitted from the |
| // OACK; they should not cause an ERROR packet to be generated. |
| } |
| |
| cur += offset; |
| left -= offset; |
| } |
| |
| char* body = resp->data; |
| memset(body, 0, *resp_len - sizeof(*resp)); |
| left = *resp_len - sizeof(*resp); |
| |
| OPCODE(session, resp, OPCODE_OACK); |
| |
| // Open file, if we haven't already |
| if (session->state == NONE) { |
| if (direction == RECV_FILE) { |
| if (!session->file_interface.open_write) { |
| xprintf("Unable to service write request: no open_write implementation\n"); |
| set_error(session, TFTP_ERR_CODE_UNDEF, resp, resp_len, "internal error"); |
| return TFTP_ERR_BAD_STATE; |
| } |
| switch (session->file_interface.open_write(session->filename, session->file_size, cookie)) { |
| case TFTP_ERR_SHOULD_WAIT: |
| // The open_write() callback can return an ERR_SHOULD_WAIT response if it isn't |
| // prepared to service another request at the moment and the client should retry |
| // later. |
| xprintf("Denying write request received when not ready\n"); |
| set_error(session, TFTP_ERR_CODE_BUSY, resp, resp_len, "not ready to receive"); |
| session->state = NONE; |
| return TFTP_ERR_SHOULD_WAIT; |
| case TFTP_NO_ERROR: |
| break; |
| default: |
| xprintf("Could not open file on write request\n"); |
| set_error(session, TFTP_ERR_CODE_ACCESS_VIOLATION, resp, resp_len, |
| "could not open file for writing"); |
| return TFTP_ERR_BAD_STATE; |
| } |
| } else { |
| ssize_t file_size; |
| if (!session->file_interface.open_read) { |
| xprintf("Unable to service read request: no open_read implementation\n"); |
| set_error(session, TFTP_ERR_CODE_UNDEF, resp, resp_len, "internal error"); |
| return TFTP_ERR_BAD_STATE; |
| } |
| |
| file_size = session->file_interface.open_read(session->filename, cookie); |
| if (file_size == TFTP_ERR_SHOULD_WAIT) { |
| // The open_read() callback can return an ERR_SHOULD_WAIT response if it isn't |
| // prepared to service another request at the moment and the client should retry |
| // later. |
| xprintf("Denying read request received when not ready\n"); |
| set_error(session, TFTP_ERR_CODE_BUSY, resp, resp_len, "not ready to send"); |
| session->state = NONE; |
| return TFTP_ERR_SHOULD_WAIT; |
| } |
| if (file_size < 0) { |
| xprintf("Unable to open file %s for reading\n", session->filename); |
| set_error(session, TFTP_ERR_CODE_FILE_NOT_FOUND, resp, resp_len, |
| "could not open file for reading"); |
| return TFTP_ERR_BAD_STATE; |
| } |
| session->file_size = file_size; |
| } |
| } |
| |
| if (file_size_seen) { |
| append_option(&body, &left, kTsize, false, "%zu", session->file_size); |
| } else { |
| xprintf("No TSIZE option specified\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "no TSIZE option"); |
| if (session->file_interface.close) { |
| session->file_interface.close(cookie); |
| } |
| return TFTP_ERR_BAD_STATE; |
| } |
| if (requested_options.mask & BLOCKSIZE_OPTION) { |
| // TODO(jpoichet) Make sure this block size is possible. Need API upwards to |
| // request allocation of block size * window size memory |
| append_option(&body, &left, kBlkSize, false, "%d", session->block_size); |
| } |
| if (requested_options.mask & TIMEOUT_OPTION) { |
| // TODO(jpoichet) Make sure this timeout is possible. Need API upwards to |
| // request allocation of block size * window size memory |
| append_option(&body, &left, kTimeout, false, "%d", session->timeout); |
| *timeout_ms = 1000 * session->timeout; |
| } |
| if (requested_options.mask & WINDOWSIZE_OPTION) { |
| append_option(&body, &left, kWindowSize, false, "%d", session->window_size); |
| } |
| *resp_len = *resp_len - left; |
| session->state = REQ_RECEIVED; |
| session->direction = direction; |
| |
| xprintf("%s Request Parsed\n", (direction == SEND_FILE) ? "Read" : "Write"); |
| xprintf(" Mode : %s\n", |
| session->mode == MODE_NETASCII |
| ? "netascii" |
| : session->mode == MODE_OCTET ? "octet" |
| : session->mode == MODE_MAIL ? "mail" : "unrecognized"); |
| xprintf(" File Size : %zu\n", session->file_size); |
| xprintf("Options requested: %08x\n", requested_options.mask); |
| xprintf(" Block Size : %d\n", requested_options.block_size); |
| xprintf(" Timeout : %d\n", requested_options.timeout); |
| xprintf(" Window Size: %d\n", requested_options.window_size); |
| xprintf("Using options\n"); |
| xprintf(" Block Size : %d\n", session->block_size); |
| xprintf(" Timeout : %d\n", session->timeout); |
| xprintf(" Window Size: %d\n", session->window_size); |
| |
| return TFTP_NO_ERROR; |
| } |
| |
| tftp_status tftp_handle_wrq(tftp_session* session, tftp_msg* wrq, size_t wrq_len, tftp_msg* resp, |
| size_t* resp_len, uint32_t* timeout_ms, void* cookie) { |
| return tftp_handle_request(session, RECV_FILE, wrq, wrq_len, resp, resp_len, timeout_ms, cookie); |
| } |
| |
| tftp_status tftp_handle_rrq(tftp_session* session, tftp_msg* rrq, size_t rrq_len, tftp_msg* resp, |
| size_t* resp_len, uint32_t* timeout_ms, void* cookie) { |
| return tftp_handle_request(session, SEND_FILE, rrq, rrq_len, resp, resp_len, timeout_ms, cookie); |
| } |
| |
| static void tftp_prepare_ack(tftp_session* session, tftp_msg* msg, size_t* msg_len) { |
| tftp_data_msg* ack_data = (tftp_data_msg*)msg; |
| xprintf(" -> Ack %" PRIu64 "\n", session->block_number); |
| session->window_index = 0; |
| OPCODE(session, ack_data, OPCODE_ACK); |
| ack_data->block = htons(session->block_number & 0xffff); |
| *msg_len = sizeof(*ack_data); |
| } |
| |
| tftp_status tftp_handle_data(tftp_session* session, tftp_msg* msg, size_t msg_len, tftp_msg* resp, |
| size_t* resp_len, uint32_t* timeout_ms, void* cookie) { |
| if ((session->direction == RECV_FILE) && |
| ((session->state == REQ_RECEIVED) || (session->state == FIRST_DATA) || |
| (session->state == RECEIVING_DATA))) { |
| session->state = RECEIVING_DATA; |
| } else { |
| set_error(session, TFTP_ERR_CODE_UNDEF, resp, resp_len, "internal error: bad state"); |
| return TFTP_ERR_INTERNAL; |
| } |
| |
| tftp_data_msg* data = (tftp_data_msg*)msg; |
| |
| uint16_t block_num = ntohs(data->block); |
| |
| // The block field of the message is only 16 bits wide. To support large files |
| // (> 65535 * blocksize bytes), we allow the block number to wrap. We use signed modulo |
| // math to determine the relative location of the block to our current position. |
| int16_t block_delta = block_num - (uint16_t)session->block_number; |
| xprintf(" <- Block %" PRIu64 " (Last = %" PRIu64 ", Offset = %" PRIu64 |
| ", Size = %zd, Left = %" PRIu64 ")\n", |
| session->block_number + block_delta, session->block_number, |
| session->block_number * session->block_size, session->file_size, |
| session->file_size - session->block_number * session->block_size); |
| if (block_delta == 1) { |
| xprintf("Advancing normally + 1\n"); |
| session->metrics.inorder_blocks++; |
| void* buf = data->data; |
| size_t len = msg_len - sizeof(tftp_data_msg); |
| size_t off = session->block_number * session->block_size; |
| while (len > 0) { |
| tftp_status ret; |
| // TODO(tkilbourn): assert that these function pointers are set |
| size_t wr = len; |
| ret = session->file_interface.write(buf, &wr, off, cookie); |
| if (ret < 0) { |
| xprintf("Error writing: %d\n", ret); |
| return ret; |
| } |
| session->metrics.inorder_bytes += wr; |
| buf += wr; |
| off += wr; |
| len -= wr; |
| } |
| session->block_number++; |
| session->window_index++; |
| } else if (block_delta > 1) { |
| session->metrics.outoforder_blocks++; |
| // Force sending a ACK with the last block_number we received |
| xprintf("Skipped: got %" PRIu64 ", expected %" PRIu64 "\n", session->block_number + block_delta, |
| session->block_number + 1); |
| session->window_index = session->window_size; |
| // It's possible that a previous ACK wasn't received, increment the prefix |
| if (session->use_opcode_prefix) { |
| session->opcode_prefix++; |
| } |
| } |
| |
| if (session->window_index == session->window_size || |
| session->block_number * session->block_size > session->file_size) { |
| tftp_prepare_ack(session, resp, resp_len); |
| if (block_delta > 1) { |
| session->metrics.nacks_sent++; |
| } else { |
| session->metrics.acks_sent++; |
| } |
| if (session->block_number * session->block_size > session->file_size) { |
| return TFTP_TRANSFER_COMPLETED; |
| } |
| } else { |
| // Nothing to send |
| *resp_len = 0; |
| } |
| return TFTP_NO_ERROR; |
| } |
| |
| tftp_status tftp_handle_ack(tftp_session* session, tftp_msg* ack, size_t ack_len, tftp_msg* resp, |
| size_t* resp_len, uint32_t* timeout_ms, void* cookie) { |
| if ((session->direction != SEND_FILE) || |
| ((session->state != FIRST_DATA) && (session->state != REQ_RECEIVED) && |
| (session->state != SENDING_DATA))) { |
| set_error(session, TFTP_ERR_CODE_UNDEF, resp, resp_len, "internal error: bad state"); |
| return TFTP_ERR_INTERNAL; |
| } |
| // Need to move forward in data and send it |
| tftp_data_msg* ack_data = (void*)ack; |
| tftp_data_msg* resp_data = (void*)resp; |
| |
| uint16_t ack_block = ntohs(ack_data->block); |
| xprintf(" <- Ack %d\n", ack_block); |
| |
| // Since we track blocks in 32 bits, but the packets only support 16 bits, calculate the |
| // signed 16 bit offset to determine the adjustment to the current position. |
| int16_t block_offset = ack_block - (uint16_t)session->block_number; |
| |
| if (session->state != FIRST_DATA && session->state != REQ_RECEIVED && block_offset == 0) { |
| session->metrics.sas_events++; |
| // Don't acknowledge duplicate ACKs, avoiding the "Sorcerer's Apprentice Syndrome" |
| *resp_len = 0; |
| return TFTP_NO_ERROR; |
| } |
| |
| if (block_offset < session->window_size) { |
| // If it looks like some of our data might have been dropped, modify the prefix |
| // before resending. |
| if (session->use_opcode_prefix) { |
| session->opcode_prefix++; |
| } |
| } |
| session->state = SENDING_DATA; |
| session->block_number += block_offset; |
| session->window_index = 0; |
| |
| if (session->block_number * session->block_size > session->file_size) { |
| *resp_len = 0; |
| return TFTP_TRANSFER_COMPLETED; |
| } |
| |
| tftp_status ret = tx_data(session, resp_data, resp_len, cookie); |
| if (ret < 0) { |
| set_error(session, TFTP_ERR_CODE_UNDEF, resp, resp_len, "could not transmit data"); |
| } |
| return ret; |
| } |
| |
| tftp_status tftp_handle_error(tftp_session* session, tftp_err_msg* err, size_t err_len, |
| tftp_msg* resp, size_t* resp_len, uint32_t* timeout_ms, |
| void* cookie) { |
| if (err_len < sizeof(tftp_err_msg)) { |
| return TFTP_ERR_BUFFER_TOO_SMALL; |
| } |
| uint16_t err_code = ntohs(err->err_code); |
| |
| // There's no need to respond to an error |
| *resp_len = 0; |
| |
| if (err_code == TFTP_ERR_CODE_BUSY) { |
| xprintf("Target busy\n"); |
| session->state = NONE; |
| return TFTP_ERR_SHOULD_WAIT; |
| } |
| xprintf("Target sent error %d\n", err_code); |
| session->state = ERROR; |
| return TFTP_ERR_INTERNAL; |
| } |
| |
| tftp_status tftp_handle_oack(tftp_session* session, tftp_msg* oack, size_t oack_len, tftp_msg* resp, |
| size_t* resp_len, uint32_t* timeout_ms, void* cookie) { |
| xprintf("Option Ack\n"); |
| if (session->state == REQ_SENT || session->state == FIRST_DATA) { |
| session->state = FIRST_DATA; |
| } |
| |
| size_t left = oack_len - sizeof(*oack); |
| char* cur = oack->data; |
| size_t offset; |
| char *option, *value; |
| |
| while (left > 0) { |
| offset = next_option(cur, left, &option, &value); |
| if (!offset) { |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "invalid option format"); |
| return TFTP_ERR_INTERNAL; |
| } |
| |
| if (!strncasecmp(option, kTsize, kTsizeLen)) { // RFC 2349 |
| if (session->direction == RECV_FILE) { |
| session->file_size = atol(value); |
| } |
| // If we are sending the file, we don't care what value the server wrote in here |
| } else if (!strncasecmp(option, kBlkSize, kBlkSizeLen)) { // RFC 2348 |
| if (!(session->client_sent_opts.mask & BLOCKSIZE_OPTION)) { |
| xprintf("block size not requested\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "no block size"); |
| return TFTP_ERR_INTERNAL; |
| } |
| // Valid values range between "8" and "65464" octets, inclusive |
| long val = atol(value); |
| if (val < 8 || val > 65464) { |
| xprintf("invalid block size\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "invalid block size"); |
| return TFTP_ERR_INTERNAL; |
| } |
| // TODO(tkilbourn): with an MTU of 1500, shouldn't be more than 1428 |
| session->block_size = val; |
| } else if (!strncasecmp(option, kTimeout, kTimeoutLen)) { // RFC 2349 |
| if (!(session->client_sent_opts.mask & TIMEOUT_OPTION)) { |
| xprintf("timeout not requested\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "no timeout"); |
| return TFTP_ERR_INTERNAL; |
| } |
| // Valid values range between "1" and "255" seconds inclusive. |
| long val = atol(value); |
| if (val < 1 || val > 255) { |
| xprintf("invalid timeout\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "invalid timeout"); |
| return TFTP_ERR_INTERNAL; |
| } |
| session->timeout = val; |
| } else if (!strncasecmp(option, kWindowSize, kWindowSizeLen)) { // RFC 7440 |
| if (!(session->client_sent_opts.mask & WINDOWSIZE_OPTION)) { |
| xprintf("window size not requested\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "no window size"); |
| return TFTP_ERR_INTERNAL; |
| } |
| // The valid values range MUST be between 1 and 65535 blocks, inclusive. |
| long val = atol(value); |
| if (val < 1 || val > 65535) { |
| xprintf("invalid window size\n"); |
| set_error(session, TFTP_ERR_CODE_BAD_OPTIONS, resp, resp_len, "invalid window size"); |
| return TFTP_ERR_INTERNAL; |
| } |
| session->window_size = val; |
| } else { |
| // Options which the server does not support should be omitted from the |
| // OACK; they should not cause an ERROR packet to be generated. |
| } |
| |
| cur += offset; |
| left -= offset; |
| } |
| *timeout_ms = 1000 * session->timeout; |
| |
| xprintf("Options negotiated\n"); |
| xprintf(" File Size : %zu\n", session->file_size); |
| xprintf(" Block Size : %d\n", session->block_size); |
| xprintf(" Timeout : %d\n", session->timeout); |
| xprintf(" Window Size: %d\n", session->window_size); |
| |
| session->offset = 0; |
| session->block_number = 0; |
| session->window_index = 0; |
| |
| if (session->direction == SEND_FILE) { |
| tftp_data_msg* resp_data = (void*)resp; |
| tftp_status ret = tx_data(session, resp_data, resp_len, cookie); |
| if (ret < 0) { |
| set_error(session, TFTP_ERR_CODE_UNDEF, resp, resp_len, "failure to transmit data"); |
| } |
| return ret; |
| } else { |
| if (!session->file_interface.open_write || |
| session->file_interface.open_write(session->filename, session->file_size, cookie)) { |
| xprintf("Could not open file on write request\n"); |
| set_error(session, TFTP_ERR_CODE_UNDEF, resp, resp_len, "could not open file for writing"); |
| return TFTP_ERR_BAD_STATE; |
| } |
| tftp_prepare_ack(session, resp, resp_len); |
| session->metrics.acks_sent++; |
| return TFTP_NO_ERROR; |
| } |
| } |
| |
| tftp_status tftp_process_msg(tftp_session* session, void* incoming, size_t inlen, void* outgoing, |
| size_t* outlen, uint32_t* timeout_ms, void* cookie) { |
| if (inlen < sizeof(tftp_msg)) { |
| return TFTP_ERR_BUFFER_TOO_SMALL; |
| } |
| |
| tftp_msg* msg = incoming; |
| tftp_msg* resp = outgoing; |
| |
| // Decode opcode |
| uint16_t opcode = ntohs(msg->opcode) & 0xff; |
| xprintf("handle_msg opcode=%u length=%d\n", opcode, (int)inlen); |
| |
| // Set default timeout |
| *timeout_ms = 1000 * session->timeout; |
| |
| // Reset timeout count |
| session->consecutive_timeouts = 0; |
| |
| switch (opcode) { |
| case OPCODE_RRQ: |
| return tftp_handle_rrq(session, incoming, inlen, resp, outlen, timeout_ms, cookie); |
| case OPCODE_WRQ: |
| return tftp_handle_wrq(session, incoming, inlen, resp, outlen, timeout_ms, cookie); |
| case OPCODE_DATA: |
| return tftp_handle_data(session, incoming, inlen, resp, outlen, timeout_ms, cookie); |
| case OPCODE_ACK: |
| return tftp_handle_ack(session, incoming, inlen, resp, outlen, timeout_ms, cookie); |
| case OPCODE_ERROR: |
| return tftp_handle_error(session, incoming, inlen, resp, outlen, timeout_ms, cookie); |
| case OPCODE_OACK: |
| return tftp_handle_oack(session, incoming, inlen, resp, outlen, timeout_ms, cookie); |
| default: |
| xprintf("Unknown opcode\n"); |
| session->state = ERROR; |
| return TFTP_ERR_INTERNAL; |
| } |
| } |
| |
| tftp_status tftp_prepare_data(tftp_session* session, void* outgoing, size_t* outlen, |
| uint32_t* timeout_ms, void* cookie) { |
| tftp_data_msg* resp_data = outgoing; |
| |
| if ((session->block_number + session->window_index) * session->block_size > session->file_size) { |
| *outlen = 0; |
| return TFTP_TRANSFER_COMPLETED; |
| } |
| |
| tftp_status ret = tx_data(session, resp_data, outlen, cookie); |
| if (ret < 0) { |
| set_error(session, TFTP_ERR_CODE_UNDEF, outgoing, outlen, "failure to transmit data"); |
| } |
| return ret; |
| } |
| |
| void tftp_session_set_max_timeouts(tftp_session* session, uint16_t max_timeouts) { |
| session->max_timeouts = max_timeouts; |
| } |
| |
| void tftp_session_set_opcode_prefix_use(tftp_session* session, bool enable) { |
| session->use_opcode_prefix = enable; |
| } |
| |
| tftp_status tftp_timeout(tftp_session* session, void* msg_buf, size_t* msg_len, size_t buf_sz, |
| uint32_t* timeout_ms, void* file_cookie) { |
| xprintf("Timeout\n"); |
| session->metrics.timeouts++; |
| if (++session->consecutive_timeouts > session->max_timeouts) { |
| return TFTP_ERR_TIMED_OUT; |
| } |
| // It's possible our previous transmission was dropped because of checksum errors. |
| // Use a different opcode prefix when we resend. |
| if (session->use_opcode_prefix) { |
| session->opcode_prefix++; |
| } |
| if (session->state == REQ_SENT || session->state == REQ_RECEIVED) { |
| // Resend previous message |
| return TFTP_NO_ERROR; |
| } |
| *msg_len = buf_sz; |
| if (session->direction == SEND_FILE) { |
| // Reset back to the last-acknowledged block |
| session->window_index = 0; |
| return tftp_prepare_data(session, msg_buf, msg_len, timeout_ms, file_cookie); |
| } else { |
| // ACK up to the last block read |
| tftp_prepare_ack(session, msg_buf, msg_len); |
| return TFTP_NO_ERROR; |
| } |
| } |
| |
| #define REPORT_ERR(opts, ...) \ |
| if (opts->err_msg) { \ |
| snprintf(opts->err_msg, opts->err_msg_sz, __VA_ARGS__); \ |
| } |
| |
| typedef struct { |
| void* incoming; |
| size_t in_buf_sz; |
| void* outgoing; |
| size_t out_buf_sz; |
| char* err_msg; |
| size_t err_msg_sz; |
| } tftp_msg_loop_opts; |
| |
| static tftp_status tftp_msg_loop(tftp_session* session, void* transport_cookie, void* file_cookie, |
| tftp_msg_loop_opts* opts, uint32_t timeout_ms) { |
| tftp_status ret; |
| size_t out_sz = 0; |
| |
| do { |
| tftp_status send_status; |
| int result = session->transport_interface.timeout_set(timeout_ms, transport_cookie); |
| if (result < 0) { |
| REPORT_ERR(opts, "failed during transport timeout set callback"); |
| return TFTP_ERR_INTERNAL; |
| } |
| |
| bool pending = tftp_session_has_pending(session); |
| int n = session->transport_interface.recv(opts->incoming, opts->in_buf_sz, !pending, |
| transport_cookie); |
| if (n == TFTP_ERR_TIMED_OUT) { |
| if (pending) { |
| out_sz = opts->out_buf_sz; |
| ret = tftp_prepare_data(session, opts->outgoing, &out_sz, &timeout_ms, file_cookie); |
| if (out_sz) { |
| send_status = session->transport_interface.send(opts->outgoing, out_sz, transport_cookie); |
| if (send_status != TFTP_NO_ERROR) { |
| REPORT_ERR(opts, "failed during transport send callback"); |
| return send_status; |
| } |
| } |
| if (ret < 0) { |
| REPORT_ERR(opts, "failed to prepare data to send"); |
| return ret; |
| } |
| } else if (session->state != NONE) { |
| ret = tftp_timeout(session, opts->outgoing, &out_sz, opts->out_buf_sz, &timeout_ms, |
| file_cookie); |
| if (ret == TFTP_ERR_TIMED_OUT) { |
| REPORT_ERR(opts, "too many consecutive timeouts, aborting"); |
| return ret; |
| } |
| if (ret < 0) { |
| REPORT_ERR(opts, "failed during timeout processing"); |
| return ret; |
| } |
| if (out_sz) { |
| send_status = session->transport_interface.send(opts->outgoing, out_sz, transport_cookie); |
| if (send_status != TFTP_NO_ERROR) { |
| REPORT_ERR(opts, "failed during transport send callback"); |
| return n; |
| } |
| } |
| } |
| continue; |
| } else if (n < 0) { |
| REPORT_ERR(opts, "failed during transport recv callback"); |
| return n; |
| } |
| |
| out_sz = opts->out_buf_sz; |
| ret = tftp_process_msg(session, opts->incoming, n, opts->outgoing, &out_sz, &timeout_ms, |
| file_cookie); |
| if (out_sz) { |
| send_status = session->transport_interface.send(opts->outgoing, out_sz, transport_cookie); |
| if (send_status != TFTP_NO_ERROR) { |
| REPORT_ERR(opts, "failed during transport send callback"); |
| return send_status; |
| } |
| } |
| if (ret < 0) { |
| REPORT_ERR(opts, "failed to handle message"); |
| return ret; |
| } else if (ret == TFTP_TRANSFER_COMPLETED) { |
| return ret; |
| } |
| } while (1); |
| } |
| |
| static tftp_status transfer_file(tftp_session* session, void* transport_cookie, void* file_cookie, |
| tftp_file_direction xfer_direction, const char* local_filename, |
| const char* remote_filename, tftp_request_opts* opts) { |
| if (!opts || !opts->inbuf || !opts->inbuf_sz || !opts->outbuf || !opts->outbuf_sz) { |
| return TFTP_ERR_INVALID_ARGS; |
| } |
| |
| tftp_status status; |
| |
| ssize_t file_size = 0; |
| if (xfer_direction == SEND_FILE) { |
| file_size = session->file_interface.open_read(local_filename, file_cookie); |
| if (file_size < 0) { |
| REPORT_ERR(opts, "failed during file open callback"); |
| return file_size; |
| } |
| } |
| |
| tftp_mode mode = opts->mode ? *opts->mode : TFTP_DEFAULT_CLIENT_MODE; |
| |
| size_t out_sz = opts->outbuf_sz; |
| uint32_t timeout_ms; |
| status = tftp_generate_request(session, xfer_direction, local_filename, remote_filename, mode, |
| file_size, opts->block_size, opts->timeout, opts->window_size, |
| opts->outbuf, &out_sz, &timeout_ms); |
| |
| const char* xfer_direction_str = (xfer_direction == SEND_FILE) ? "write" : "read"; |
| if (status < 0) { |
| REPORT_ERR(opts, "failed to generate %s request", xfer_direction_str); |
| goto done; |
| } |
| if (!out_sz) { |
| REPORT_ERR(opts, "no %s request generated", xfer_direction_str); |
| status = TFTP_ERR_INTERNAL; |
| goto done; |
| } |
| |
| status = session->transport_interface.send(opts->outbuf, out_sz, transport_cookie); |
| if (status != TFTP_NO_ERROR) { |
| REPORT_ERR(opts, "failed during transport send callback"); |
| goto done; |
| } |
| |
| tftp_msg_loop_opts msg_loop_opts = {.incoming = opts->inbuf, |
| .in_buf_sz = opts->inbuf_sz, |
| .outgoing = opts->outbuf, |
| .out_buf_sz = opts->outbuf_sz, |
| .err_msg = opts->err_msg, |
| .err_msg_sz = opts->err_msg_sz}; |
| status = tftp_msg_loop(session, transport_cookie, file_cookie, &msg_loop_opts, timeout_ms); |
| |
| done: |
| if ((xfer_direction == SEND_FILE) || (session->state != NONE)) { |
| if (session->file_interface.close) { |
| session->file_interface.close(file_cookie); |
| } |
| } |
| return status; |
| } |
| |
| tftp_status tftp_push_file(tftp_session* session, void* transport_cookie, void* file_cookie, |
| const char* local_filename, const char* remote_filename, |
| tftp_request_opts* opts) { |
| return transfer_file(session, transport_cookie, file_cookie, SEND_FILE, local_filename, |
| remote_filename, opts); |
| } |
| |
| tftp_status tftp_pull_file(tftp_session* session, void* transport_cookie, void* file_cookie, |
| const char* local_filename, const char* remote_filename, |
| tftp_request_opts* opts) { |
| return transfer_file(session, transport_cookie, file_cookie, RECV_FILE, local_filename, |
| remote_filename, opts); |
| } |
| |
| tftp_status tftp_service_request(tftp_session* session, void* transport_cookie, void* file_cookie, |
| tftp_handler_opts* opts) { |
| if (!opts || !opts->inbuf || !opts->outbuf || !opts->outbuf_sz) { |
| return TFTP_ERR_INVALID_ARGS; |
| } |
| tftp_msg_loop_opts msg_loop_opts = {.incoming = opts->inbuf, |
| .in_buf_sz = opts->inbuf_sz, |
| .outgoing = opts->outbuf, |
| .out_buf_sz = *opts->outbuf_sz, |
| .err_msg = opts->err_msg, |
| .err_msg_sz = opts->err_msg_sz}; |
| uint32_t timeout_ms = session->timeout * 1000; |
| tftp_status status = |
| tftp_msg_loop(session, transport_cookie, file_cookie, &msg_loop_opts, timeout_ms); |
| if ((session->state != NONE) && session->file_interface.close) { |
| session->file_interface.close(file_cookie); |
| } |
| return status; |
| } |
| |
| tftp_status tftp_handle_msg(tftp_session* session, void* transport_cookie, void* file_cookie, |
| tftp_handler_opts* opts) { |
| if (!opts || !opts->inbuf || !opts->outbuf || !opts->outbuf_sz) { |
| return TFTP_ERR_INVALID_ARGS; |
| } |
| uint32_t timeout_ms; |
| tftp_status ret; |
| ret = tftp_process_msg(session, opts->inbuf, opts->inbuf_sz, opts->outbuf, opts->outbuf_sz, |
| &timeout_ms, file_cookie); |
| if (*opts->outbuf_sz) { |
| tftp_status send_status = |
| session->transport_interface.send(opts->outbuf, *opts->outbuf_sz, transport_cookie); |
| if (send_status != TFTP_NO_ERROR) { |
| REPORT_ERR(opts, "failed during transport send callback"); |
| return send_status; |
| } |
| } |
| if (ret == TFTP_ERR_SHOULD_WAIT) { |
| REPORT_ERR(opts, "request received, host is busy"); |
| } else if (ret < 0) { |
| REPORT_ERR(opts, "handling tftp request failed (file might not exist)"); |
| } else if (ret == TFTP_TRANSFER_COMPLETED) { |
| if (session->file_interface.close) { |
| session->file_interface.close(file_cookie); |
| } |
| } else { |
| ret = session->transport_interface.timeout_set(timeout_ms, transport_cookie); |
| if (ret < 0) { |
| REPORT_ERR(opts, "failed during transport timeout set callback"); |
| } |
| } |
| return ret; |
| } |
| |
| tftp_status tftp_get_metrics(tftp_session* session, char* buf, size_t buf_sz) { |
| snprintf(buf, buf_sz, |
| "{" |
| "\"inorderblks\": %u," |
| "\"oooblks\": %u," |
| "\"ack\": %u," |
| "\"nack\": %u," |
| "\"timeouts\": %u," |
| "\"sas\": %u," |
| "\"inorderbytes\": %" PRIu64 "}", |
| session->metrics.inorder_blocks, session->metrics.outoforder_blocks, |
| session->metrics.acks_sent, session->metrics.nacks_sent, session->metrics.timeouts, |
| session->metrics.sas_events, session->metrics.inorder_bytes); |
| if (strlen(buf) == buf_sz - 1) { |
| return TFTP_ERR_BUFFER_TOO_SMALL; |
| } |
| return TFTP_NO_ERROR; |
| } |
| |
| void tftp_clear_metrics(tftp_session* session) { |
| memset(&session->metrics, 0, sizeof(session->metrics)); |
| } |