Line data Source code
1 : /*
2 : GCS MAVLink functions related to FTP
3 :
4 : This program is free software: you can redistribute it and/or modify
5 : it under the terms of the GNU General Public License as published by
6 : the Free Software Foundation, either version 3 of the License, or
7 : (at your option) any later version.
8 :
9 : This program is distributed in the hope that it will be useful,
10 : but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : GNU General Public License for more details.
13 :
14 : You should have received a copy of the GNU General Public License
15 : along with this program. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : #include "GCS_config.h"
19 :
20 : #if AP_MAVLINK_FTP_ENABLED
21 :
22 : #include "GCS_FTP.h"
23 :
24 : #include <AP_HAL/AP_HAL.h>
25 :
26 : #include "GCS.h"
27 :
28 : #include <AP_Filesystem/AP_Filesystem.h>
29 : #include <AP_HAL/utility/sparse-endian.h>
30 : #include <AP_BoardConfig/AP_BoardConfig.h>
31 :
32 : extern const AP_HAL::HAL& hal;
33 :
34 : GCS_FTP *GCS_FTP::ftp;
35 :
36 : // timeout for session inactivity, when we will kill the session if
37 : // the session slot is needed
38 : #define FTP_SESSION_TIMEOUT 3000
39 :
40 : // timeout for session inactivity, when we will kill an idle session
41 : #define FTP_SESSION_KILL_TIMEOUT 20000
42 :
43 125 : bool GCS_FTP::init(void)
44 : {
45 125 : if (initialised) {
46 118 : return true;
47 : }
48 :
49 7 : initialised = hal.scheduler->thread_create(FUNCTOR_BIND_MEMBER(&GCS_FTP::worker, void),
50 : "FTP", 2560, AP_HAL::Scheduler::PRIORITY_IO, 0);
51 7 : if (!initialised) {
52 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "failed to initialize MAVFTP");
53 : }
54 :
55 7 : return initialised;
56 : }
57 :
58 : /*
59 : handle a FILE_TRANSFER_PROTOCOL message
60 : */
61 125 : void GCS_FTP::handle_file_transfer_protocol(const mavlink_message_t &msg, mavlink_channel_t chan)
62 : {
63 : #if !defined(HAL_BUILD_AP_PERIPH)
64 : // check if ftp is disabled for memory savings
65 125 : if (AP_BoardConfig::ftp_disabled()) {
66 0 : GCS_SEND_TEXT(MAV_SEVERITY_INFO, "FTP disabled");
67 0 : return;
68 : }
69 : #endif
70 125 : if (ftp == nullptr) {
71 7 : ftp = NEW_NOTHROW GCS_FTP;
72 7 : if (ftp == nullptr) {
73 0 : return;
74 : }
75 : }
76 125 : if (ftp->init()) {
77 : mavlink_file_transfer_protocol_t packet;
78 125 : mavlink_msg_file_transfer_protocol_decode(&msg, &packet);
79 :
80 : Transaction request;
81 :
82 125 : request.chan = chan;
83 125 : request.seq_number = le16toh_ptr(packet.payload);
84 :
85 125 : request.session = packet.payload[2];
86 125 : request.opcode = static_cast<FTP_OP>(packet.payload[3]);
87 125 : request.size = packet.payload[4];
88 125 : request.req_opcode = static_cast<FTP_OP>(packet.payload[5]);
89 125 : request.burst_complete = packet.payload[6];
90 125 : request.offset = le32toh_ptr(&packet.payload[8]);
91 125 : request.sysid = msg.sysid;
92 125 : request.compid = msg.compid;
93 125 : memcpy(request.data, &packet.payload[12], sizeof(packet.payload) - 12);
94 :
95 : // if the push fails we drop the message
96 : // we could NACK it, but that can lead to GCS
97 : // confusion, so we're treating it like lost data
98 125 : ftp->requests.push(request);
99 : }
100 : }
101 :
102 771 : bool GCS_FTP::send_reply(const Transaction &reply)
103 : {
104 771 : if (!GCS_MAVLINK::last_txbuf_is_greater(33)) { // It helps avoid GCS timeout if this is less than the threshold where we slow down normal streams (<=49)
105 0 : return false;
106 : }
107 771 : WITH_SEMAPHORE(comm_chan_lock(reply.chan));
108 771 : if (!HAVE_PAYLOAD_SPACE(reply.chan, FILE_TRANSFER_PROTOCOL)) {
109 0 : return false;
110 : }
111 771 : mavlink_file_transfer_protocol_t pkt {};
112 771 : pkt.target_network = 0;
113 771 : pkt.target_system = reply.sysid;
114 771 : pkt.target_component = reply.compid;
115 771 : uint8_t *payload = pkt.payload;
116 771 : put_le16_ptr(payload, reply.seq_number);
117 771 : payload[2] = reply.session;
118 771 : payload[3] = static_cast<uint8_t>(reply.opcode);
119 771 : payload[4] = reply.size;
120 771 : payload[5] = static_cast<uint8_t>(reply.req_opcode);
121 771 : payload[6] = reply.burst_complete ? 1 : 0;
122 771 : put_le32_ptr(&payload[8], reply.offset);
123 771 : memcpy(&pkt.payload[12], reply.data, sizeof(reply.data));
124 771 : mavlink_msg_file_transfer_protocol_send_struct(reply.chan, &pkt);
125 771 : return true;
126 771 : }
127 :
128 : /*
129 : check a name length for validity
130 : */
131 39 : bool GCS_FTP::Session::check_name_len(const Transaction &request)
132 : {
133 39 : const size_t file_name_len = strnlen((char *)request.data, sizeof(request.data));
134 39 : if (request.size == 0) {
135 2 : return false;
136 : }
137 37 : if (file_name_len == request.size) {
138 10 : return true;
139 : }
140 27 : return (request.size - file_name_len == 1) && (request.data[sizeof(request.data) - 1] == 0);
141 : }
142 :
143 : // send our response back out to the system
144 752 : void GCS_FTP::Session::push_reply(Transaction &reply)
145 : {
146 752 : last_send_ms = AP_HAL::millis(); // Used to detect active FTP session
147 :
148 752 : while (!send_reply(reply)) {
149 0 : hal.scheduler->delay_microseconds(100);
150 : }
151 :
152 752 : if (reply.req_opcode == FTP_OP::TerminateSession) {
153 21 : last_send_ms = 0;
154 : }
155 752 : }
156 :
157 : // calculates how much string length is needed to fit this in a list response
158 294 : int GCS_FTP::Session::gen_dir_entry(char *dest, size_t space, const char *path, const struct dirent * entry)
159 : {
160 : #if AP_FILESYSTEM_HAVE_DIRENT_DTYPE
161 294 : const bool is_file = entry->d_type == DT_REG || entry->d_type == DT_LNK;
162 : #else
163 : // assume true initially, then handle below
164 : const bool is_file = true;
165 : #endif
166 :
167 294 : if (space < 3) {
168 0 : return -1;
169 : }
170 294 : dest[0] = 0;
171 :
172 : #if AP_FILESYSTEM_HAVE_DIRENT_DTYPE
173 294 : if (!is_file && entry->d_type != DT_DIR) {
174 0 : return -1; // this just forces it so we can't send this back, it's easier then sending skips to a GCS
175 : }
176 : #endif
177 :
178 294 : if (is_file) {
179 : #ifdef MAX_NAME_LEN
180 430 : const uint8_t max_name_len = MIN(unsigned(MAX_NAME_LEN), 255U);
181 : #else
182 : const uint8_t max_name_len = 255U;
183 : #endif
184 215 : const size_t full_path_len = strlen(path) + strnlen(entry->d_name, max_name_len);
185 215 : char full_path[full_path_len + 2];
186 215 : hal.util->snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);
187 : struct stat st;
188 215 : if (AP::FS().stat(full_path, &st)) {
189 141 : return -1;
190 : }
191 :
192 : #if !AP_FILESYSTEM_HAVE_DIRENT_DTYPE
193 : if (S_ISDIR(st.st_mode)) {
194 : return hal.util->snprintf(dest, space, "D%s%c", entry->d_name, (char)0);
195 : }
196 : #endif
197 74 : return hal.util->snprintf(dest, space, "F%s\t%u%c", entry->d_name, (unsigned)st.st_size, (char)0);
198 215 : } else {
199 79 : return hal.util->snprintf(dest, space, "D%s%c", entry->d_name, (char)0);
200 : }
201 : }
202 :
203 : // list the contents of a directory, skip the offset number of entries before providing data
204 12 : void GCS_FTP::Session::list_dir(Transaction &request, Transaction &response)
205 : {
206 12 : response.offset = request.offset; // this should be set for any failure condition for debugging
207 :
208 : // sanity check that our the request looks well formed
209 12 : if (!check_name_len(request)) {
210 1 : GCS_FTP::error(response, FTP_ERROR::InvalidDataSize);
211 1 : return;
212 : }
213 :
214 11 : request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
215 :
216 : // Strip trailing /
217 11 : const size_t dir_len = strlen((char *)request.data);
218 11 : if ((dir_len > 1) && (request.data[dir_len - 1] == '/')) {
219 2 : request.data[dir_len - 1] = 0;
220 : }
221 :
222 : // open the dir
223 11 : auto *dir = AP::FS().opendir((char *)request.data);
224 11 : if (dir == nullptr) {
225 1 : GCS_FTP::error(response, FTP_ERROR::FailErrno);
226 1 : return;
227 : }
228 :
229 : // burn the entries we don't care about
230 167 : while (request.offset > 0) {
231 158 : const struct dirent *entry = AP::FS().readdir(dir);
232 158 : if(entry == nullptr) {
233 1 : GCS_FTP::error(response, FTP_ERROR::EndOfFile);
234 1 : AP::FS().closedir(dir);
235 1 : return;
236 : }
237 :
238 : // check how much space would be needed to emit the listing
239 157 : const int needed_space = gen_dir_entry((char *)response.data, sizeof(request.data), (char *)request.data, entry);
240 :
241 : // an entry needing the whole packet still does not fit, as the
242 : // packing loop below only takes an entry which leaves the index
243 : // inside the buffer. both loops must agree on which entries are
244 : // skipped or the offsets they are counting drift apart
245 157 : if (needed_space < 0 || needed_space >= (int)sizeof(request.data)) {
246 86 : continue;
247 : }
248 :
249 71 : request.offset--;
250 : }
251 :
252 : // start packing in entries that fit
253 9 : uint8_t index = 0;
254 : struct dirent *entry;
255 144 : while ((entry = AP::FS().readdir(dir))) {
256 : // figure out if we can fit the file
257 137 : const int required_space = gen_dir_entry((char *)(response.data + index), sizeof(response.data) - index, (char *)request.data, entry);
258 :
259 : // couldn't ever send this so drop it
260 137 : if (required_space < 0) {
261 71 : continue;
262 : }
263 :
264 : // this entry doesn't fit in a packet of its own, so no later list
265 : // will be able to send it either. dropping it loses one file from
266 : // the listing; breaking here would end the listing at an EndOfFile
267 : // and lose every file after it as well
268 66 : if (required_space >= (int)sizeof(response.data)) {
269 8 : continue;
270 : }
271 :
272 : // can't fit it in this one, leave it for the next list to send
273 58 : if ((required_space + index) >= (int)sizeof(request.data)) {
274 2 : break;
275 : }
276 :
277 : // step the index forward and keep going. required_space already
278 : // includes the entry's null terminator (the trailing "%c" with a 0),
279 : // so we must not add another - a stray second null would appear as an
280 : // empty entry to clients, inflating their offset across paged listings.
281 56 : index += required_space;
282 : }
283 :
284 9 : if (index == 0) {
285 3 : GCS_FTP::error(response, FTP_ERROR::EndOfFile);
286 3 : AP::FS().closedir(dir);
287 3 : return;
288 : }
289 :
290 : // strip any bad temp data from our response as it can confuse a GCS, and defeats 0 trimming
291 6 : if (index < sizeof(response.data)) {
292 6 : memset(response.data + index, 0, MAX(0, (int)(sizeof(response.data)) - index));
293 : }
294 :
295 6 : response.opcode = FTP_OP::Ack;
296 6 : response.size = index;
297 :
298 6 : AP::FS().closedir(dir);
299 : }
300 :
301 : /*
302 : close a session
303 :
304 : returns the error code friom the underlying close() call, or zero (no error) if the
305 : file was closed already
306 : */
307 53 : int GCS_FTP::Session::close(void)
308 : {
309 53 : int result = 0;
310 :
311 53 : if (fd != -1) {
312 15 : result = AP::FS().close(fd);
313 15 : fd = -1;
314 : }
315 53 : last_send_ms = 0;
316 :
317 53 : return result;
318 : }
319 :
320 : /*
321 : handle one request on a session
322 :
323 : return true if the main loop should skip the push_replies() call
324 : */
325 105 : bool GCS_FTP::Session::handle_request(Transaction &request, Transaction &reply)
326 : {
327 105 : bool skip_push_reply = false;
328 :
329 : // sanity check the request size
330 105 : if (request.size > sizeof(request.data)) {
331 1 : GCS_FTP::error(reply, FTP_ERROR::InvalidDataSize);
332 1 : return false;
333 : }
334 :
335 104 : const uint32_t now = AP_HAL::millis();
336 :
337 : // dispatch the command as needed
338 104 : switch (request.opcode) {
339 0 : case FTP_OP::None:
340 0 : reply.opcode = FTP_OP::Ack;
341 0 : break;
342 21 : case FTP_OP::TerminateSession:
343 21 : if (close()) {
344 : // close() operation indicated an error, errno
345 : // was set by close() itself
346 0 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
347 : } else {
348 21 : reply.opcode = FTP_OP::Ack;
349 : }
350 21 : break;
351 12 : case FTP_OP::ListDirectory:
352 12 : list_dir(request, reply);
353 12 : break;
354 13 : case FTP_OP::OpenFileRO:
355 : {
356 : // only allow one file to be open per session
357 13 : if (fd != -1 && now - last_send_ms > FTP_SESSION_TIMEOUT) {
358 : // no activity for 3s, assume client has
359 : // timed out receiving open reply, close
360 : // the file
361 0 : close(); // error code ignored
362 0 : fd = -1;
363 : }
364 13 : if (fd != -1) {
365 0 : GCS_FTP::error(reply, FTP_ERROR::Fail);
366 0 : break;
367 : }
368 :
369 : // sanity check that the request looks well formed
370 13 : if (!check_name_len(request)) {
371 0 : GCS_FTP::error(reply, FTP_ERROR::InvalidDataSize);
372 0 : break;
373 : }
374 :
375 13 : request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
376 :
377 : // get the file size
378 : struct stat st;
379 13 : if (AP::FS().stat((char *)request.data, &st)) {
380 0 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
381 0 : break;
382 : }
383 13 : const size_t file_size = st.st_size;
384 :
385 : // actually open the file
386 13 : fd = AP::FS().open((char *)request.data, O_RDONLY);
387 13 : if (fd == -1) {
388 0 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
389 0 : break;
390 : }
391 13 : mode = FTP_FILE_MODE::Read;
392 :
393 13 : reply.opcode = FTP_OP::Ack;
394 13 : reply.size = sizeof(uint32_t);
395 13 : put_le32_ptr(reply.data, (uint32_t)file_size);
396 :
397 : // provide compatibility with old protocol banner download
398 13 : if (strncmp((const char *)request.data, "@PARAM/param.pck", 16) == 0) {
399 2 : auto *gchan = gcs().chan(reply.chan);
400 2 : if (gchan != nullptr) {
401 2 : gchan->send_banner();
402 : }
403 : }
404 13 : break;
405 : }
406 22 : case FTP_OP::ReadFile:
407 : {
408 : // must actually be working on a file
409 22 : if (fd == -1) {
410 1 : GCS_FTP::error(reply, FTP_ERROR::FileNotFound);
411 1 : break;
412 : }
413 :
414 : // must have the file in read mode
415 21 : if ((mode != FTP_FILE_MODE::Read)) {
416 1 : GCS_FTP::error(reply, FTP_ERROR::Fail);
417 1 : break;
418 : }
419 :
420 : // seek to requested offset
421 20 : if (AP::FS().lseek(fd, request.offset, SEEK_SET) == -1) {
422 0 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
423 0 : break;
424 : }
425 :
426 : // fill the buffer
427 20 : const ssize_t read_bytes = AP::FS().read(fd, reply.data, MIN(sizeof(reply.data),request.size));
428 20 : if (read_bytes == -1) {
429 0 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
430 0 : break;
431 : }
432 20 : if (read_bytes == 0) {
433 1 : GCS_FTP::error(reply, FTP_ERROR::EndOfFile);
434 1 : break;
435 : }
436 :
437 19 : reply.opcode = FTP_OP::Ack;
438 19 : reply.offset = request.offset;
439 19 : reply.size = (uint8_t)read_bytes;
440 19 : break;
441 : }
442 0 : case FTP_OP::Ack:
443 : case FTP_OP::Nack:
444 : // eat these, we just didn't expect them
445 0 : return true;
446 :
447 3 : case FTP_OP::OpenFileWO:
448 : case FTP_OP::CreateFile:
449 : {
450 : // only allow one file to be open per session
451 3 : if (fd != -1) {
452 0 : GCS_FTP::error(reply, FTP_ERROR::Fail);
453 0 : break;
454 : }
455 :
456 : // sanity check that our the request looks well formed
457 3 : if (!check_name_len(request)) {
458 0 : GCS_FTP::error(reply, FTP_ERROR::InvalidDataSize);
459 0 : break;
460 : }
461 :
462 3 : request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
463 :
464 : // actually open the file
465 6 : fd = AP::FS().open((char *)request.data,
466 3 : (request.opcode == FTP_OP::CreateFile) ? O_WRONLY|O_CREAT|O_TRUNC : O_WRONLY|O_CREAT);
467 3 : if (fd == -1) {
468 0 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
469 0 : break;
470 : }
471 3 : mode = FTP_FILE_MODE::Write;
472 :
473 3 : reply.opcode = FTP_OP::Ack;
474 3 : break;
475 : }
476 4 : case FTP_OP::WriteFile:
477 : {
478 : // must actually be working on a file
479 4 : if (fd == -1) {
480 0 : GCS_FTP::error(reply, FTP_ERROR::FileNotFound);
481 0 : break;
482 : }
483 :
484 : // must have the file in write mode
485 4 : if ((mode != FTP_FILE_MODE::Write)) {
486 0 : GCS_FTP::error(reply, FTP_ERROR::Fail);
487 0 : break;
488 : }
489 :
490 : // seek to requested offset
491 4 : if (AP::FS().lseek(fd, request.offset, SEEK_SET) == -1) {
492 0 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
493 0 : break;
494 : }
495 :
496 : // fill the buffer
497 4 : const ssize_t write_bytes = AP::FS().write(fd, request.data, request.size);
498 4 : if (write_bytes == -1) {
499 0 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
500 0 : break;
501 : }
502 :
503 4 : reply.opcode = FTP_OP::Ack;
504 4 : reply.offset = request.offset;
505 4 : break;
506 : }
507 4 : case FTP_OP::CreateDirectory:
508 : {
509 : // sanity check that our the request looks well formed
510 4 : if (!check_name_len(request)) {
511 0 : GCS_FTP::error(reply, FTP_ERROR::InvalidDataSize);
512 0 : break;
513 : }
514 :
515 4 : request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
516 :
517 : // actually make the directory
518 4 : if (AP::FS().mkdir((char *)request.data) == -1) {
519 2 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
520 2 : break;
521 : }
522 :
523 2 : reply.opcode = FTP_OP::Ack;
524 2 : break;
525 : }
526 3 : case FTP_OP::RemoveDirectory:
527 : case FTP_OP::RemoveFile:
528 : {
529 : // sanity check that our the request looks well formed
530 3 : if (!check_name_len(request)) {
531 0 : GCS_FTP::error(reply, FTP_ERROR::InvalidDataSize);
532 0 : break;
533 : }
534 :
535 3 : request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
536 :
537 : // remove the file/dir
538 3 : if (AP::FS().unlink((char *)request.data) == -1) {
539 0 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
540 0 : break;
541 : }
542 :
543 3 : reply.opcode = FTP_OP::Ack;
544 3 : break;
545 : }
546 4 : case FTP_OP::CalcFileCRC32:
547 : {
548 : // sanity check that our the request looks well formed
549 4 : if (!check_name_len(request)) {
550 1 : GCS_FTP::error(reply, FTP_ERROR::InvalidDataSize);
551 1 : break;
552 : }
553 :
554 3 : request.data[sizeof(request.data) - 1] = 0; // ensure the path is null terminated
555 :
556 3 : uint32_t checksum = 0;
557 3 : if (!AP::FS().crc32((char *)request.data, checksum)) {
558 1 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
559 1 : break;
560 : }
561 :
562 : // reset our scratch area so we don't leak data, and can leverage trimming
563 2 : memset(reply.data, 0, sizeof(reply.data));
564 2 : reply.size = sizeof(uint32_t);
565 2 : put_le32_ptr(reply.data, checksum);
566 2 : reply.opcode = FTP_OP::Ack;
567 2 : break;
568 : }
569 12 : case FTP_OP::BurstReadFile:
570 : {
571 12 : const uint16_t max_read = (request.size == 0?sizeof(reply.data):request.size);
572 : // must actually be working on a file
573 12 : if (fd == -1) {
574 0 : GCS_FTP::error(reply, FTP_ERROR::FileNotFound);
575 0 : break;
576 : }
577 :
578 : // must have the file in read mode
579 12 : if ((mode != FTP_FILE_MODE::Read)) {
580 0 : GCS_FTP::error(reply, FTP_ERROR::Fail);
581 0 : break;
582 : }
583 :
584 : // seek to requested offset
585 12 : if (AP::FS().lseek(fd, request.offset, SEEK_SET) == -1) {
586 1 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
587 1 : break;
588 : }
589 :
590 : /*
591 : calculate a burst delay so that FTP burst
592 : transfer doesn't use more than 1/3 of
593 : available bandwidth on links that don't have
594 : flow control. This reduces the chance of
595 : lost packets a lot, which results in overall
596 : faster transfers
597 : */
598 11 : uint32_t burst_delay_ms = 0;
599 11 : if (valid_channel(request.chan)) {
600 11 : auto *port = mavlink_comm_port[request.chan];
601 11 : if (port != nullptr && port->get_flow_control() != AP_HAL::UARTDriver::FLOW_CONTROL_ENABLE) {
602 0 : const uint32_t bw = port->bw_in_bytes_per_second();
603 0 : const uint16_t pkt_size = PAYLOAD_SIZE(request.chan, FILE_TRANSFER_PROTOCOL) - (sizeof(reply.data) - max_read);
604 0 : burst_delay_ms = 3000 * pkt_size / bw;
605 : }
606 : }
607 :
608 : // this transfer size is enough for a full parameter file with max parameters
609 11 : const uint32_t transfer_size = 2000;
610 11 : reply.offset = request.offset;
611 657 : for (uint32_t i = 0; (i < transfer_size); i++) {
612 : // fill the buffer
613 657 : const ssize_t read_bytes = AP::FS().read(fd, reply.data, MIN(sizeof(reply.data), max_read));
614 657 : if (read_bytes == -1) {
615 0 : reply.burst_complete = true;
616 0 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
617 0 : break;
618 : }
619 :
620 657 : if (read_bytes != sizeof(reply.data)) {
621 : // don't send any old data
622 408 : memset(reply.data + read_bytes, 0, sizeof(reply.data) - read_bytes);
623 : }
624 :
625 657 : if (read_bytes == 0) {
626 11 : reply.burst_complete = true;
627 11 : GCS_FTP::error(reply, FTP_ERROR::EndOfFile);
628 11 : break;
629 : }
630 :
631 646 : reply.opcode = FTP_OP::Ack;
632 : // Signal to the client that they need to request another burst read to get more data
633 646 : reply.burst_complete = (i == (transfer_size - 1));
634 646 : reply.size = (uint8_t)read_bytes;
635 :
636 646 : push_reply(reply);
637 :
638 : // update the offset for the next read
639 646 : reply.offset += read_bytes;
640 :
641 : // prep the reply to be used again
642 646 : reply.seq_number++;
643 :
644 646 : if (burst_delay_ms > 0) {
645 0 : hal.scheduler->delay(burst_delay_ms);
646 : }
647 : }
648 :
649 11 : if (reply.opcode != FTP_OP::Nack) {
650 : // prevent a duplicate packet send for
651 : // normal replies of burst reads
652 0 : skip_push_reply = true;
653 : }
654 11 : break;
655 : }
656 :
657 5 : case FTP_OP::Rename: {
658 : // sanity check that the request looks well formed
659 5 : const char *filename1 = (char*)request.data;
660 5 : const size_t len1 = strnlen(filename1, sizeof(request.data)-2);
661 5 : const char *filename2 = (char*)&request.data[len1+1];
662 5 : const size_t len2 = strnlen(filename2, sizeof(request.data)-(len1+1));
663 6 : const bool is_req_size_consider_tnull = (request.size - (len1+len2) == 2 &&
664 1 : request.data[sizeof(request.data) - 1] == 0);
665 5 : if (filename1[len1] != 0 || ((len1+len2+1 != request.size) && !is_req_size_consider_tnull) || (request.size == 0)) {
666 1 : GCS_FTP::error(reply, FTP_ERROR::InvalidDataSize);
667 1 : break;
668 : }
669 4 : request.data[sizeof(request.data) - 1] = 0; // ensure the 2nd path is null terminated
670 : // remove the file/dir
671 4 : if (AP::FS().rename(filename1, filename2) != 0) {
672 1 : GCS_FTP::error(reply, FTP_ERROR::FailErrno);
673 1 : break;
674 : }
675 3 : reply.opcode = FTP_OP::Ack;
676 3 : break;
677 : }
678 :
679 1 : case FTP_OP::TruncateFile:
680 : default:
681 : // we don't implement this opcode. say so specifically, so a client
682 : // preferring a newer opcode can tell "never heard of it" from "that
683 : // command failed" and fall back to the older one
684 1 : GCS_SEND_TEXT(MAV_SEVERITY_DEBUG, "Unsupported FTP: %d", static_cast<int>(request.opcode));
685 1 : GCS_FTP::error(reply, FTP_ERROR::UnknownCommand);
686 1 : break;
687 : }
688 :
689 104 : return skip_push_reply;
690 : }
691 :
692 : /*
693 : get the time of the last send for a channel
694 : */
695 103052786 : uint32_t GCS_FTP::get_last_send_ms(mavlink_channel_t chan)
696 : {
697 103052786 : if (ftp == nullptr) {
698 101326675 : return 0;
699 : }
700 1726111 : uint32_t ret = 0;
701 10356666 : for (const auto &s : ftp->sessions) {
702 : // using a comparison will be briefly wrong every 49 days, but
703 : // this is non-critical and getting it perfect would be
704 : // expensive in a hot path
705 8630555 : if (s.chan == chan && s.last_send_ms > ret) {
706 29953 : ret = s.last_send_ms;
707 : }
708 : }
709 1726111 : return ret;
710 : }
711 :
712 : /*
713 : fill in a reply with an error code
714 : */
715 29 : void GCS_FTP::error(Transaction &response, FTP_ERROR error)
716 : {
717 29 : response.opcode = FTP_OP::Nack;
718 29 : response.data[0] = static_cast<uint8_t>(error);
719 29 : response.size = 1;
720 :
721 : // FIXME: errno's are not thread-local as they should be on ChibiOS
722 29 : if (error == FTP_ERROR::FailErrno) {
723 : // translate the errno's that we have useful messages for
724 6 : switch (errno) {
725 2 : case EEXIST:
726 2 : response.data[0] = static_cast<uint8_t>(FTP_ERROR::FileExists);
727 2 : break;
728 3 : case ENOENT:
729 3 : response.data[0] = static_cast<uint8_t>(FTP_ERROR::FileNotFound);
730 3 : break;
731 1 : default:
732 1 : response.data[1] = static_cast<uint8_t>(errno);
733 1 : response.size = 2;
734 1 : break;
735 : }
736 : }
737 29 : }
738 :
739 : /*
740 : setup reply packet to reply to the request
741 : */
742 124 : void GCS_FTP::setup_reply(const Transaction &request, Transaction &reply)
743 : {
744 124 : memset(&reply, 0, sizeof(reply));
745 124 : reply.req_opcode = request.opcode;
746 124 : reply.session = request.session;
747 124 : reply.seq_number = request.seq_number + 1;
748 124 : reply.chan = request.chan;
749 124 : reply.sysid = request.sysid;
750 124 : reply.compid = request.compid;
751 124 : }
752 :
753 : /*
754 : main FTP thread
755 : */
756 7 : void GCS_FTP::worker(void)
757 : {
758 : Transaction request;
759 7 : Transaction reply {};
760 7 : reply.session = -1; // flag the reply as invalid for any reuse
761 :
762 : while (true) {
763 483879 : while (!requests.pop(request)) {
764 : // nothing to handle, delay ourselves a bit then check again. Ideally we'd use conditional waits here
765 483754 : hal.scheduler->delay(2);
766 :
767 : // kill any dead sessions
768 483747 : const uint32_t now = AP_HAL::millis();
769 2902482 : for (auto &s : sessions) {
770 2418735 : if (s.last_send_ms != 0 &&
771 17469 : now - s.last_send_ms > FTP_SESSION_KILL_TIMEOUT) {
772 2 : s.close(); // error code ignored
773 : }
774 : }
775 : }
776 :
777 125 : if (request.opcode == FTP_OP::ResetSessions) {
778 : /*
779 : close all sessions for this channel, compid and sysid
780 : */
781 114 : for (auto &s : sessions) {
782 95 : if (request.sysid == s.sysid &&
783 16 : request.compid == s.compid &&
784 16 : request.chan == s.chan) {
785 : // close this session
786 16 : s.close(); // error code ignored
787 : }
788 : }
789 : // always ACK, even if no sessions were closed
790 19 : setup_reply(request, reply);
791 19 : reply.opcode = FTP_OP::Ack;
792 19 : send_reply(reply);
793 19 : continue;
794 19 : }
795 :
796 106 : Session *session = nullptr;
797 227 : for (uint8_t i=0; i<ARRAY_SIZE(sessions); i++) {
798 213 : auto &s = sessions[i];
799 213 : if (request.sysid == s.sysid &&
800 98 : request.compid == s.compid &&
801 98 : request.chan == s.chan &&
802 98 : request.session == s.session_id) {
803 : // found the session
804 92 : session = &s;
805 92 : break;
806 : }
807 : }
808 :
809 106 : if (session == nullptr) {
810 : /*
811 : find the oldest session to possibly reuse
812 : */
813 14 : const uint32_t now = AP_HAL::millis();
814 14 : session = &sessions[0];
815 70 : for (uint8_t i=1; i<ARRAY_SIZE(sessions); i++) {
816 56 : auto &s = sessions[i];
817 56 : if ((now - s.last_send_ms) > (now - session->last_send_ms)) {
818 1 : session = &s;
819 : }
820 : }
821 :
822 : // only reuse the session if it is not active
823 14 : auto &s = *session;
824 14 : if (s.last_send_ms != 0 &&
825 0 : now - s.last_send_ms < FTP_SESSION_TIMEOUT) {
826 : // the oldest session is still active, reject the request
827 0 : setup_reply(request, reply);
828 0 : error(reply, FTP_ERROR::NoSessionsAvailable);
829 0 : send_reply(reply);
830 0 : continue;
831 : }
832 : // claim the session
833 14 : s.close(); // error code ignored
834 14 : s.session_id = request.session;
835 14 : s.sysid = request.sysid;
836 14 : s.compid = request.compid;
837 14 : s.chan = request.chan;
838 : }
839 :
840 : // if it's a rerequest and we still have the last response then send it
841 106 : if ((request.sysid == reply.sysid) && (request.compid == reply.compid) &&
842 100 : (request.session == reply.session) && (request.seq_number + 1 == reply.seq_number) &&
843 1 : reply.data[0] != uint8_t(FTP_ERROR::NoSessionsAvailable)) {
844 1 : session->push_reply(reply);
845 1 : continue;
846 : }
847 :
848 105 : setup_reply(request, reply);
849 :
850 105 : bool skip_push_reply = session->handle_request(request, reply);
851 :
852 105 : if (!skip_push_reply) {
853 105 : session->push_reply(reply);
854 : }
855 125 : }
856 : }
857 :
858 : #endif // AP_MAVLINK_FTP_ENABLED
|