blob: ea534511ffafa079765f6aa141c2d712228c1215 [file] [log] [blame] [edit]
// 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
#define _GNU_SOURCE
#define _DARWIN_C_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
#include <zircon/boot/netboot.h>
#include <tftp/tftp.h>
#include "bootserver.h"
// Point to user-selected values (or NULL if no values selected)
uint16_t* tftp_block_size;
uint16_t* tftp_window_size;
typedef struct {
int fd;
const char* data;
size_t datalen;
} xferdata;
void file_init(xferdata* xd) {
xd->fd = -1;
xd->data = NULL;
xd->datalen = 0;
}
ssize_t file_open_read(const char* filename, void* cookie) {
xferdata* xd = cookie;
if (strcmp(filename, "(cmdline)")) {
xd->fd = open(filename, O_RDONLY);
if (xd->fd < 0) {
fprintf(stderr, "%s: error: Could not open file %s\n", appname, filename);
return TFTP_ERR_NOT_FOUND;
}
struct stat st;
if (fstat(xd->fd, &st) < 0) {
fprintf(stderr, "%s: error: Could not stat %s\n", appname, filename);
goto err;
}
xd->datalen = st.st_size;
}
initialize_status(filename, xd->datalen);
return xd->datalen;
err:
if (xd->fd >= 0) {
close(xd->fd);
xd->fd = -1;
}
return TFTP_ERR_IO;
}
tftp_status file_open_write(const char* filename, size_t len, void* cookie) {
xferdata* xd = cookie;
xd->fd = open(filename, O_WRONLY | O_CREAT);
if (xd->fd < 0) {
fprintf(stderr, "%s: error: Could not open file %s\n", appname, filename);
return TFTP_ERR_NOT_FOUND;
}
if (ftruncate(xd->fd, len) < 0) {
fprintf(stderr, "%s: error: Could not ftruncate %s\n", appname, filename);
goto err;
}
xd->datalen = len;
initialize_status(filename, xd->datalen);
return TFTP_NO_ERROR;
err:
if (xd->fd >= 0) {
close(xd->fd);
xd->fd = -1;
}
return TFTP_ERR_IO;
}
tftp_status file_read(void* data, size_t* length, off_t offset, void* cookie) {
xferdata* xd = cookie;
if (xd->fd < 0) {
if (((size_t)offset > xd->datalen) || (offset + *length > xd->datalen)) {
return TFTP_ERR_IO;
}
memcpy(data, &xd->data[offset], *length);
} else {
ssize_t bytes_read = pread(xd->fd, data, *length, offset);
if (bytes_read < 0) {
return TFTP_ERR_IO;
}
*length = bytes_read;
}
update_status(offset + *length);
return TFTP_NO_ERROR;
}
tftp_status file_write(const void* data, size_t* length, off_t offset, void* cookie) {
xferdata* xd = cookie;
ssize_t bytes_written = pwrite(xd->fd, data, *length, offset);
if (bytes_written < 0) {
return TFTP_ERR_IO;
}
*length = bytes_written;
update_status(offset + *length);
return TFTP_NO_ERROR;
}
void file_close(void* cookie) {
xferdata* xd = cookie;
if (xd->fd >= 0) {
close(xd->fd);
xd->fd = -1;
}
}
typedef struct {
int socket;
bool connected;
uint32_t previous_timeout_ms;
struct sockaddr_in6 target_addr;
} transport_state;
#define SEND_TIMEOUT_US 1000
tftp_status transport_send(void* data, size_t len, void* cookie) {
transport_state* state = cookie;
ssize_t send_result;
do {
struct pollfd poll_fds = {.fd = state->socket, .events = POLLOUT, .revents = 0};
int poll_result = poll(&poll_fds, 1, SEND_TIMEOUT_US);
if (poll_result < 0) {
return TFTP_ERR_IO;
}
if (!state->connected) {
state->target_addr.sin6_port = htons(NB_TFTP_INCOMING_PORT);
send_result = sendto(state->socket, data, len, 0, (struct sockaddr*)&state->target_addr,
sizeof(state->target_addr));
} else {
send_result = send(state->socket, data, len, 0);
}
} while ((send_result < 0) &&
((errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == ENOBUFS)));
if (send_result < 0) {
fprintf(stderr, "\n%s: Attempted to send %zu bytes\n", appname, len);
switch (errno) {
case EADDRNOTAVAIL:
fprintf(stderr, "%s: Network configuration error: %s (%d)\n", appname, strerror(errno),
errno);
break;
default:
fprintf(stderr, "%s: Send failed with errno = %d (%s)\n", appname, errno, strerror(errno));
}
return TFTP_ERR_IO;
}
return TFTP_NO_ERROR;
}
int transport_recv(void* data, size_t len, bool block, void* cookie) {
transport_state* state = cookie;
int flags = fcntl(state->socket, F_GETFL, 0);
if (flags < 0) {
return TFTP_ERR_IO;
}
int new_flags;
if (block) {
new_flags = flags & ~O_NONBLOCK;
} else {
new_flags = flags | O_NONBLOCK;
}
if ((new_flags != flags) && (fcntl(state->socket, F_SETFL, new_flags) != 0)) {
return TFTP_ERR_IO;
}
ssize_t recv_result;
struct sockaddr_in6 connection_addr;
socklen_t addr_len = sizeof(connection_addr);
if (!state->connected) {
recv_result =
recvfrom(state->socket, data, len, 0, (struct sockaddr*)&connection_addr, &addr_len);
} else {
recv_result = recv(state->socket, data, len, 0);
}
if (recv_result < 0) {
if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
return TFTP_ERR_TIMED_OUT;
}
return TFTP_ERR_INTERNAL;
}
if (!state->connected) {
if (connect(state->socket, (struct sockaddr*)&connection_addr, sizeof(connection_addr)) < 0) {
return TFTP_ERR_IO;
}
memcpy(&state->target_addr, &connection_addr, sizeof(state->target_addr));
state->connected = true;
}
return recv_result;
}
int transport_timeout_set(uint32_t timeout_ms, void* cookie) {
transport_state* state = cookie;
if (state->previous_timeout_ms != timeout_ms && timeout_ms > 0) {
state->previous_timeout_ms = timeout_ms;
struct timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = 1000 * (timeout_ms - 1000 * tv.tv_sec);
return setsockopt(state->socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
}
return 0;
}
int transport_init(transport_state* state, uint32_t timeout_ms, struct sockaddr_in6* addr) {
state->socket = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
if (state->socket < 0) {
fprintf(stderr, "%s: error: Cannot create socket %d\n", appname, errno);
return -1;
}
state->previous_timeout_ms = 0;
if (transport_timeout_set(timeout_ms, state) != 0) {
fprintf(stderr, "%s: error: Unable to set socket timeout\n", appname);
goto err;
}
state->connected = false;
memcpy(&state->target_addr, addr, sizeof(struct sockaddr_in6));
return 0;
err:
close(state->socket);
state->socket = -1;
return -1;
}
#define INITIAL_CONNECTION_TIMEOUT 250
#define TFTP_BUF_SZ 2048
int tftp_xfer(struct sockaddr_in6* addr, const char* fn, const char* name, bool push) {
int result = -1;
xferdata xd;
file_init(&xd);
if (!strcmp(fn, "(cmdline)")) {
xd.data = name;
xd.datalen = strlen(name) + 1;
name = NB_CMDLINE_FILENAME;
}
void* session_data = NULL;
char* inbuf = NULL;
char* outbuf = NULL;
transport_state ts = {
.socket = -1,
.connected = false,
.previous_timeout_ms = 0,
.target_addr = {0},
};
tftp_session* session = NULL;
size_t session_data_sz = tftp_sizeof_session();
if (!(session_data = calloc(session_data_sz, 1)) || !(inbuf = malloc(TFTP_BUF_SZ)) ||
!(outbuf = malloc(TFTP_BUF_SZ))) {
fprintf(stderr, "%s: error: Unable to allocate memory\n", appname);
goto done;
}
if (tftp_init(&session, session_data, session_data_sz) != TFTP_NO_ERROR) {
fprintf(stderr, "%s: error: Unable to initialize tftp session\n", appname);
goto done;
}
tftp_file_interface file_ifc = {file_open_read, file_open_write, file_read, file_write,
file_close};
tftp_session_set_file_interface(session, &file_ifc);
if (transport_init(&ts, INITIAL_CONNECTION_TIMEOUT, addr) < 0) {
goto done;
}
tftp_transport_interface transport_ifc = {transport_send, transport_recv, transport_timeout_set};
tftp_session_set_transport_interface(session, &transport_ifc);
uint16_t default_block_size = DEFAULT_TFTP_BLOCK_SZ;
uint16_t default_window_size = DEFAULT_TFTP_WIN_SZ;
tftp_set_options(session, &default_block_size, NULL, &default_window_size);
char err_msg[128];
tftp_request_opts opts = {0};
opts.inbuf_sz = TFTP_BUF_SZ;
opts.inbuf = inbuf;
opts.outbuf_sz = TFTP_BUF_SZ;
opts.outbuf = outbuf;
opts.err_msg = err_msg;
opts.err_msg_sz = sizeof(err_msg);
opts.block_size = tftp_block_size;
opts.window_size = tftp_window_size;
tftp_status status;
if (push) {
status = tftp_push_file(session, &ts, &xd, fn, name, &opts);
} else {
status = tftp_pull_file(session, &ts, &xd, fn, name, &opts);
}
if (status < 0) {
if (status == TFTP_ERR_SHOULD_WAIT) {
result = -EAGAIN;
} else {
fprintf(stderr, "\n%s: %s (status = %d)\n", appname, opts.err_msg, (int)status);
result = -1;
}
} else {
result = 0;
}
done:
if (ts.socket >= 0) {
close(ts.socket);
}
if (session_data) {
free(session_data);
}
if (inbuf) {
free(inbuf);
}
if (outbuf) {
free(outbuf);
}
file_close(&xd);
return result;
}