blob: 3913d133dd3089aa3ee4354114fd87f693c3cc5c [file] [log] [blame]
// 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);
}
int 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));
}