/*
GCS MAVLink functions related to FTP
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
#include
#include "GCS.h"
#include
#include
#include
extern const AP_HAL::HAL& hal;
struct GCS_MAVLINK::ftp_state GCS_MAVLINK::ftp;
// timeout for session inactivity
#define FTP_SESSION_TIMEOUT 3000
bool GCS_MAVLINK::ftp_init(void) {
// check if ftp is disabled for memory savings
#if !defined(HAL_BUILD_AP_PERIPH)
if (AP_BoardConfig::ftp_disabled()) {
goto failed;
}
#endif
// we can simply check if we allocated everything we need
if (ftp.requests != nullptr) {
return true;
}
ftp.requests = new ObjectBuffer(5);
if (ftp.requests == nullptr || ftp.requests->get_size() == 0) {
goto failed;
}
if (!hal.scheduler->thread_create(FUNCTOR_BIND_MEMBER(&GCS_MAVLINK::ftp_worker, void),
"FTP", 2560, AP_HAL::Scheduler::PRIORITY_IO, 0)) {
goto failed;
}
return true;
failed:
delete ftp.requests;
ftp.requests = nullptr;
gcs().send_text(MAV_SEVERITY_WARNING, "failed to initialize MAVFTP");
return false;
}
void GCS_MAVLINK::handle_file_transfer_protocol(const mavlink_message_t &msg) {
if (ftp_init()) {
mavlink_file_transfer_protocol_t packet;
mavlink_msg_file_transfer_protocol_decode(&msg, &packet);
struct pending_ftp request;
request.chan = chan;
request.seq_number = le16toh_ptr(packet.payload);
request.session = packet.payload[2];
request.opcode = static_cast(packet.payload[3]);
request.size = packet.payload[4];
request.req_opcode = static_cast(packet.payload[5]);
request.burst_complete = packet.payload[6];
request.offset = le32toh_ptr(&packet.payload[8]);
request.sysid = msg.sysid;
request.compid = msg.compid;
memcpy(request.data, &packet.payload[12], sizeof(packet.payload) - 12);
if (!ftp.requests->push(request)) {
// dropping the message, no buffer space to queue it in
// we could NACK it, but that can lead to GCS confusion, so we're treating it like lost data
}
}
}
bool GCS_MAVLINK::send_ftp_reply(const pending_ftp &reply)
{
/*
provide same banner we would give with old param download
*/
if (ftp.need_banner_send_mask & (1U<(reply.opcode);
payload[4] = reply.size;
payload[5] = static_cast(reply.req_opcode);
payload[6] = reply.burst_complete ? 1 : 0;
put_le32_ptr(&payload[8], reply.offset);
memcpy(&payload[12], reply.data, sizeof(reply.data));
mavlink_msg_file_transfer_protocol_send(
reply.chan,
0, reply.sysid, reply.compid,
payload);
if (reply.req_opcode == FTP_OP::TerminateSession) {
ftp.last_send_ms = 0;
} else {
ftp.last_send_ms = AP_HAL::millis();
}
return true;
}
void GCS_MAVLINK::ftp_error(struct pending_ftp &response, FTP_ERROR error) {
response.opcode = FTP_OP::Nack;
response.data[0] = static_cast(error);
response.size = 1;
// FIXME: errno's are not thread-local as they should be on ChibiOS
if (error == FTP_ERROR::FailErrno) {
// translate the errno's that we have useful messages for
switch (errno) {
case EEXIST:
response.data[0] = static_cast(FTP_ERROR::FileExists);
break;
case ENOENT:
response.data[0] = static_cast(FTP_ERROR::FileNotFound);
break;
default:
response.data[1] = static_cast(errno);
response.size = 2;
break;
}
}
}
// send our response back out to the system
void GCS_MAVLINK::ftp_push_replies(pending_ftp &reply)
{
while (!send_ftp_reply(reply)) {
hal.scheduler->delay(2);
}
}
void GCS_MAVLINK::ftp_worker(void) {
pending_ftp request;
pending_ftp reply = {};
reply.session = -1; // flag the reply as invalid for any reuse
while (true) {
bool skip_push_reply = false;
while (ftp.requests == nullptr || !ftp.requests->pop(request)) {
// nothing to handle, delay ourselves a bit then check again. Ideally we'd use conditional waits here
hal.scheduler->delay(2);
}
// if it's a rerequest and we still have the last response then send it
if ((request.sysid == reply.sysid) && (request.compid = reply.compid) &&
(request.session == reply.session) && (request.seq_number + 1 == reply.seq_number)) {
ftp_push_replies(reply);
continue;
}
// setup the response
memset(&reply, 0, sizeof(reply));
reply.req_opcode = request.opcode;
reply.session = request.session;
reply.seq_number = request.seq_number + 1;
reply.chan = request.chan;
reply.sysid = request.sysid;
reply.compid = request.compid;
// sanity check the request size
if (request.size > sizeof(request.data)) {
ftp_error(reply, FTP_ERROR::InvalidDataSize);
ftp_push_replies(reply);
continue;
}
uint32_t now = AP_HAL::millis();
// check for session termination
if (request.session != ftp.current_session &&
(request.opcode == FTP_OP::TerminateSession || request.opcode == FTP_OP::ResetSessions)) {
// terminating a different session, just ack
reply.opcode = FTP_OP::Ack;
} else if (ftp.fd != -1 && request.session != ftp.current_session &&
now - ftp.last_send_ms < FTP_SESSION_TIMEOUT) {
// if we have an open file and the session isn't right
// then reject. This prevents IO on the wrong file
ftp_error(reply, FTP_ERROR::InvalidSession);
} else {
if (ftp.fd != -1 &&
request.session != ftp.current_session &&
now - ftp.last_send_ms >= FTP_SESSION_TIMEOUT) {
// if a new session appears and the old session has
// been idle for more than the timeout then force
// close the old session
AP::FS().close(ftp.fd);
ftp.fd = -1;
ftp.current_session = -1;
}
// dispatch the command as needed
switch (request.opcode) {
case FTP_OP::None:
reply.opcode = FTP_OP::Ack;
break;
case FTP_OP::TerminateSession:
case FTP_OP::ResetSessions:
// we already handled this, just listed for completeness
if (ftp.fd != -1) {
AP::FS().close(ftp.fd);
ftp.fd = -1;
}
ftp.current_session = -1;
reply.opcode = FTP_OP::Ack;
break;
case FTP_OP::ListDirectory:
ftp_list_dir(request, reply);
break;
case FTP_OP::OpenFileRO:
{
// only allow one file to be open per session
if (ftp.fd != -1 && now - ftp.last_send_ms > FTP_SESSION_TIMEOUT) {
// no activity for 3s, assume client has
// timed out receiving open reply, close
// the file
AP::FS().close(ftp.fd);
ftp.fd = -1;
ftp.current_session = -1;
}
if (ftp.fd != -1) {
ftp_error(reply, FTP_ERROR::Fail);
break;
}
// sanity check that our the request looks well formed
const size_t file_name_len = strnlen((char *)request.data, sizeof(request.data));
if ((file_name_len != request.size) || (request.size == 0)) {
ftp_error(reply, FTP_ERROR::InvalidDataSize);
break;
}
request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
// get the file size
struct stat st;
if (AP::FS().stat((char *)request.data, &st)) {
ftp_error(reply, FTP_ERROR::FailErrno);
break;
}
const size_t file_size = st.st_size;
// actually open the file
ftp.fd = AP::FS().open((char *)request.data, 0);
if (ftp.fd == -1) {
ftp_error(reply, FTP_ERROR::FailErrno);
break;
}
ftp.mode = FTP_FILE_MODE::Read;
ftp.current_session = request.session;
reply.opcode = FTP_OP::Ack;
reply.size = sizeof(uint32_t);
put_le32_ptr(reply.data, (uint32_t)file_size);
// provide compatibility with old protocol banner download
if (strncmp((const char *)request.data, "@PARAM/param.pck", 16) == 0) {
ftp.need_banner_send_mask |= 1U< 0);
AP::FS().close(fd);
// reset our scratch area so we don't leak data, and can leverage trimming
memset(reply.data, 0, sizeof(reply.data));
reply.size = sizeof(uint32_t);
put_le32_ptr(reply.data, checksum);
reply.opcode = FTP_OP::Ack;
break;
}
case FTP_OP::BurstReadFile:
{
const uint16_t max_read = (request.size == 0?sizeof(reply.data):request.size);
// must actually be working on a file
if (ftp.fd == -1) {
ftp_error(reply, FTP_ERROR::FileNotFound);
break;
}
// must have the file in read mode
if ((ftp.mode != FTP_FILE_MODE::Read)) {
ftp_error(reply, FTP_ERROR::Fail);
break;
}
// seek to requested offset
if (AP::FS().lseek(ftp.fd, request.offset, SEEK_SET) == -1) {
ftp_error(reply, FTP_ERROR::FailErrno);
break;
}
/*
calculate a burst delay so that FTP burst
transfer doesn't use more than 1/3 of
available bandwidth on links that don't have
flow control. This reduces the chance of
lost packets a lot, which results in overall
faster transfers
*/
uint32_t burst_delay_ms = 0;
if (valid_channel(request.chan)) {
auto *port = mavlink_comm_port[request.chan];
if (port != nullptr && port->get_flow_control() != AP_HAL::UARTDriver::FLOW_CONTROL_ENABLE) {
const uint32_t bw = port->bw_in_bytes_per_second();
const uint16_t pkt_size = PAYLOAD_SIZE(request.chan, FILE_TRANSFER_PROTOCOL) - (sizeof(reply.data) - max_read);
burst_delay_ms = 3000 * pkt_size / bw;
}
}
// this transfer size is enough for a full parameter file with max parameters
const uint32_t transfer_size = 500;
for (uint32_t i = 0; (i < transfer_size); i++) {
// fill the buffer
const ssize_t read_bytes = AP::FS().read(ftp.fd, reply.data, MIN(sizeof(reply.data), max_read));
if (read_bytes == -1) {
ftp_error(reply, FTP_ERROR::FailErrno);
break;
}
if (read_bytes != sizeof(reply.data)) {
// don't send any old data
memset(reply.data + read_bytes, 0, sizeof(reply.data) - read_bytes);
}
if (read_bytes == 0) {
ftp_error(reply, FTP_ERROR::EndOfFile);
break;
}
reply.opcode = FTP_OP::Ack;
reply.offset = request.offset + i * max_read;
reply.burst_complete = (i == (transfer_size - 1));
reply.size = (uint8_t)read_bytes;
ftp_push_replies(reply);
if (read_bytes < max_read) {
// ensure the NACK which we send next is at the right offset
reply.offset += read_bytes;
}
// prep the reply to be used again
reply.seq_number++;
hal.scheduler->delay(burst_delay_ms);
}
if (reply.opcode != FTP_OP::Nack) {
// prevent a duplicate packet send for
// normal replies of burst reads
skip_push_reply = true;
}
break;
}
case FTP_OP::Rename: {
// sanity check that the request looks well formed
const char *filename1 = (char*)request.data;
const size_t len1 = strnlen(filename1, sizeof(request.data)-2);
const char *filename2 = (char*)&request.data[len1+1];
const size_t len2 = strnlen(filename2, sizeof(request.data)-(len1+1));
if (filename1[len1] != 0 || (len1+len2+1 != request.size) || (request.size == 0)) {
ftp_error(reply, FTP_ERROR::InvalidDataSize);
break;
}
request.data[sizeof(request.data) - 1] = 0; // ensure the 2nd path is null terminated
// remove the file/dir
if (AP::FS().rename(filename1, filename2) != 0) {
ftp_error(reply, FTP_ERROR::FailErrno);
break;
}
reply.opcode = FTP_OP::Ack;
break;
}
case FTP_OP::TruncateFile:
default:
// this was bad data, just nack it
gcs().send_text(MAV_SEVERITY_DEBUG, "Unsupported FTP: %d", static_cast(request.opcode));
ftp_error(reply, FTP_ERROR::Fail);
break;
}
}
if (!skip_push_reply) {
ftp_push_replies(reply);
}
continue;
}
}
// calculates how much string length is needed to fit this in a list response
int GCS_MAVLINK::gen_dir_entry(char *dest, size_t space, const char *path, const struct dirent * entry) {
const bool is_file = entry->d_type == DT_REG || entry->d_type == DT_LNK;
if (space < 3) {
return -1;
}
dest[0] = 0;
if (!is_file && entry->d_type != DT_DIR) {
return -1; // this just forces it so we can't send this back, it's easier then sending skips to a GCS
}
if (is_file) {
#ifdef MAX_NAME_LEN
const uint8_t max_name_len = MIN(unsigned(MAX_NAME_LEN), 255U);
#else
const uint8_t max_name_len = 255U;
#endif
const size_t full_path_len = strlen(path) + strnlen(entry->d_name, max_name_len);
char full_path[full_path_len + 2];
hal.util->snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
struct stat st;
if (AP::FS().stat(full_path, &st)) {
return -1;
}
return hal.util->snprintf(dest, space, "F%s\t%u%c", entry->d_name, (unsigned)st.st_size, (char)0);
} else {
return hal.util->snprintf(dest, space, "D%s%c", entry->d_name, (char)0);
}
}
// list the contents of a directory, skip the offset number of entries before providing data
void GCS_MAVLINK::ftp_list_dir(struct pending_ftp &request, struct pending_ftp &response) {
response.offset = request.offset; // this should be set for any failure condition for debugging
const size_t directory_name_size = strnlen((char *)request.data, sizeof(request.data));
// sanity check that our the request looks well formed
if ((directory_name_size != request.size) || (request.size == 0)) {
ftp_error(response, FTP_ERROR::InvalidDataSize);
return;
}
request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
// open the dir
auto *dir = AP::FS().opendir((char *)request.data);
if (dir == nullptr) {
ftp_error(response, FTP_ERROR::FailErrno);
return;
}
// burn the entries we don't care about
while (request.offset > 0) {
const struct dirent *entry = AP::FS().readdir(dir);
if(entry == nullptr) {
ftp_error(response, FTP_ERROR::EndOfFile);
AP::FS().closedir(dir);
return;
}
// check how much space would be needed to emit the listing
const int needed_space = gen_dir_entry((char *)response.data, sizeof(request.data), (char *)request.data, entry);
if (needed_space < 0 || needed_space > (int)sizeof(request.data)) {
continue;
}
request.offset--;
}
// start packing in entries that fit
uint8_t index = 0;
struct dirent *entry;
while ((entry = AP::FS().readdir(dir))) {
// figure out if we can fit the file
const int required_space = gen_dir_entry((char *)(response.data + index), sizeof(response.data) - index, (char *)request.data, entry);
// couldn't ever send this so drop it
if (required_space < 0) {
continue;
}
// can't fit it in this one, leave it for the next list to send
if ((required_space + index) >= (int)sizeof(request.data)) {
break;
}
// step the index forward and keep going
index += required_space + 1;
}
if (index == 0) {
ftp_error(response, FTP_ERROR::EndOfFile);
AP::FS().closedir(dir);
return;
}
// strip any bad temp data from our response as it can confuse a GCS, and defeats 0 trimming
if (index < sizeof(response.data)) {
memset(response.data + index, 0, MAX(0, (int)(sizeof(response.data)) - index));
}
response.opcode = FTP_OP::Ack;
response.size = index;
AP::FS().closedir(dir);
}