ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ssh_sftp.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ssh_sftp.cpp
6 * @brief SFTP server subsystem - fs::FS binding. See ssh_sftp.h.
7 *
8 * Binds the pure SFTP v3 codec (services/file_transfer/sftp) to an SSH session channel: accumulates SSH_FXP_* request
9 * packets from the channel byte stream, executes them against an fs::FS mount, and frames responses back with
10 * pc_ssh_conn_send. A large WRITE is streamed straight to the file (never buffered whole); a READ returns a short
11 * DATA (the client re-requests). A fixed handle table holds open files/dirs. Every path is checked for `..`
12 * traversal (server/fs_path.h) before touching the filesystem.
13 */
14
15#include "server/ssh_sftp.h"
16#include "shared_primitives/strbuf.h" // pc_sb frame builder
17
18#if PC_ENABLE_SSH_SFTP
19
21#include "network_drivers/presentation/ssh/connection/ssh_conn.h" // pc_ssh_conn_send / pc_ssh_conn_close_channel
22#include "server/fs_path.h"
24#include <string.h>
25
26namespace
27{
28// Leave headroom below one SSH packet for the CHANNEL_DATA framing, so pc_ssh_conn_send never rejects a response.
29#define PC_SFTP_RESP_CAP (SSH_PKT_BUF_SIZE - 16)
30// Worst-case one READDIR NAME entry (filename + longname + attrs), used to stash an entry that did not fit.
31#define PC_SFTP_ENTRY_MAX (PC_SFTP_PATH_MAX + 320)
32
33struct SftpHandle
34{
35 bool used;
36 bool is_dir;
37 fs::File file; ///< the open fs handle (a dir cursor for READDIR)
38 char path[PC_SFTP_PATH_MAX]; ///< resolved on-disk path
39 bool readdir_done; ///< the directory has been fully listed
40 bool has_pending; ///< a READDIR entry that did not fit last time, emitted first next time
41 uint16_t pend_len;
42 uint8_t pend[PC_SFTP_ENTRY_MAX];
43};
44
45struct SftpSession
46{
47 bool active;
48 uint8_t slot;
49 uint32_t channel;
50 uint16_t acc_len; ///< bytes accumulated toward the next request packet
51 uint8_t acc[PC_SFTP_PKT_BUF];
52 // streaming write: a WRITE whose data payload arrives across CHANNEL_DATA calls
53 bool writing;
54 int wr_handle;
55 uint64_t wr_off;
56 uint32_t wr_remaining;
57 uint32_t wr_id;
58 bool wr_err;
59 SftpHandle handles[PC_SFTP_MAX_HANDLES];
60};
61
62struct SshSftpCtx
63{
64 fs::FS *fs = nullptr;
65 const char *root = "/";
66 bool registered = false;
67 SftpSession sess[MAX_SSH_CONNS];
68 uint8_t out[SSH_PKT_BUF_SIZE]; ///< response-build scratch (sends are synchronous, one at a time)
69 uint8_t rbuf[PC_SFTP_MAX_READ]; ///< READ scratch
70};
71SshSftpCtx s_sftp;
72
73// --- handle table ---------------------------------------------------------------------------------
74void free_handle(SftpSession *s, int h)
75{
76 if (h < 0 || h >= PC_SFTP_MAX_HANDLES || !s->handles[h].used)
77 {
78 return;
79 }
80 if (s->handles[h].file)
81 {
82 s->handles[h].file.close();
83 }
84 s->handles[h].file = fs::File();
85 s->handles[h].used = false;
86 s->handles[h].has_pending = false;
87}
88void free_all_handles(SftpSession *s)
89{
90 for (int i = 0; i < PC_SFTP_MAX_HANDLES; i++)
91 {
92 free_handle(s, i);
93 }
94}
95int alloc_handle(SftpSession *s)
96{
97 for (int i = 0; i < PC_SFTP_MAX_HANDLES; i++)
98 {
99 if (!s->handles[i].used)
100 {
101 return i;
102 }
103 }
104 return -1;
105}
106// A handle string is our 4-byte big-endian table index. @return the valid index, or -1.
107int handle_index(SftpSession *s, const uint8_t *h, uint32_t hl)
108{
109 if (hl != 4)
110 {
111 return -1;
112 }
113 uint32_t idx = ((uint32_t)h[0] << 24) | ((uint32_t)h[1] << 16) | ((uint32_t)h[2] << 8) | (uint32_t)h[3];
114 if (idx >= PC_SFTP_MAX_HANDLES || !s->handles[idx].used)
115 {
116 return -1;
117 }
118 return (int)idx;
119}
120
121// --- helpers --------------------------------------------------------------------------------------
122const char *base_name(const char *name)
123{
124 const char *slash = strrchr(name, '/');
125 return slash ? slash + 1 : name;
126}
127
128void attrs_from_file(fs::File &f, SftpAttrs *a)
129{
130 bool dir = f.isDirectory();
131 a->flags = PC_SSH_FILEXFER_ATTR_SIZE | PC_SSH_FILEXFER_ATTR_PERMS | PC_SSH_FILEXFER_ATTR_ACMODTIME;
132 a->size = dir ? 0 : (uint64_t)f.size();
133 a->permissions = dir ? (PC_SFTP_S_IFDIR | 0755) : (PC_SFTP_S_IFREG | 0644);
134 uint32_t mt = (uint32_t)f.getLastWrite();
135 a->atime = mt;
136 a->mtime = mt;
137}
138
139// Resolve a client request path to an on-disk path under the mount root. "" and "." mean the root.
140int resolve(const uint8_t *path, uint32_t plen, char *out, size_t cap)
141{
142 char req[PC_SFTP_PATH_MAX];
143 if (plen >= sizeof(req))
144 {
145 return -2;
146 }
147 memcpy(req, path, plen);
148 req[plen] = '\0';
149 const char *sub = (plen == 0 || (plen == 1 && req[0] == '.')) ? "/" : req;
150 return fs_path_resolve(s_sftp.root, sub, out, cap);
151}
152
153void send_resp(SftpSession *s, size_t n)
154{
155 if (n > 0)
156 {
157 pc_ssh_conn_send(s->slot, s->channel, s_sftp.out, n);
158 }
159}
160void send_status(SftpSession *s, uint32_t id, uint32_t code, const char *msg)
161{
162 send_resp(s, pc_sftp_build_status(id, code, msg, s_sftp.out, PC_SFTP_RESP_CAP));
163}
164void send_handle(SftpSession *s, uint32_t id, int hi)
165{
166 uint8_t hb[4] = {(uint8_t)((uint32_t)hi >> 24), (uint8_t)((uint32_t)hi >> 16), (uint8_t)((uint32_t)hi >> 8),
167 (uint8_t)hi};
168 send_resp(s, pc_sftp_build_handle(id, hb, 4, s_sftp.out, PC_SFTP_RESP_CAP));
169}
170// Map an fs errno-free open failure to a plausible SFTP status: a write open that fails is FAILURE, a read
171// open that fails is NO_SUCH_FILE (the common case a client distinguishes).
172uint32_t open_fail_code(bool writing)
173{
174 return writing ? PC_SSH_FX_FAILURE : PC_SSH_FX_NO_SUCH_FILE;
175}
176
177// --- write streaming ------------------------------------------------------------------------------
178void write_stream_bytes(SftpSession *s, const uint8_t *data, size_t n)
179{
180 if (n == 0)
181 {
182 return;
183 }
184 if (!s->wr_err && s->wr_handle >= 0)
185 {
186 fs::File &f = s->handles[s->wr_handle].file;
187 if (f.write(data, n) != n)
188 {
189 s->wr_err = true;
190 }
191 }
192 s->wr_off += n;
193 s->wr_remaining -= (uint32_t)n;
194}
195void finish_write(SftpSession *s)
196{
197 send_status(s, s->wr_id, s->wr_err ? PC_SSH_FX_FAILURE : PC_SSH_FX_OK, s->wr_err ? "write failed" : "");
198 s->writing = false;
199}
200
201// --- READDIR --------------------------------------------------------------------------------------
202// Serialize one directory entry (filename + longname + attrs) into @p ent; @return its length.
203size_t build_entry(fs::File &c, uint8_t *ent, size_t cap)
204{
205 const char *base = base_name(c.name());
206 SftpAttrs a;
207 attrs_from_file(c, &a);
208 char ln[PC_SFTP_ENTRY_MAX];
209 pc_sftp_format_longname(c.isDirectory(), a.permissions, a.size, a.mtime, base, ln, sizeof(ln));
210 SftpWriter w;
211 pc_sftp_wr_init(&w, ent, cap); // reserves a 4-byte length prefix we discard below
212 pc_sftp_wr_string(&w, base, (uint32_t)strnlen(base, PC_SFTP_ENTRY_MAX));
213 pc_sftp_wr_string(&w, ln, (uint32_t)strnlen(ln, sizeof(ln)));
214 pc_sftp_wr_attrs(&w, &a);
215 if (w.ovf)
216 {
217 return 0;
218 }
219 size_t el = w.off - 4;
220 memmove(ent, ent + 4, el); // drop the reserved prefix so the entry bytes start at ent[0]
221 return el;
222}
223
224void do_readdir(SftpSession *s, uint32_t id, SftpHandle *H)
225{
226 if (H->readdir_done && !H->has_pending)
227 {
228 send_status(s, id, PC_SSH_FX_EOF, "");
229 return;
230 }
231 SftpWriter w;
232 pc_sftp_wr_init(&w, s_sftp.out, PC_SFTP_RESP_CAP);
233 pc_sftp_wr_u8(&w, PC_SSH_FXP_NAME);
234 pc_sftp_wr_u32(&w, id);
235 size_t count_at = pc_sftp_wr_pos(&w);
236 pc_sftp_wr_u32(&w, 0); // count placeholder
237 uint32_t count = 0;
238
239 if (H->has_pending) // an entry that did not fit last call
240 {
241 pc_sftp_wr_bytes(&w, H->pend, H->pend_len);
242 H->has_pending = false;
243 count++;
244 }
245 while (!H->readdir_done)
246 {
247 fs::File c = H->file.openNextFile();
248 if (!c)
249 {
250 H->readdir_done = true;
251 break;
252 }
253 uint8_t ent[PC_SFTP_ENTRY_MAX];
254 size_t el = build_entry(c, ent, sizeof(ent));
255 c.close();
256 if (el == 0)
257 {
258 continue; // entry could not be serialized (pathological name) - skip it
259 }
260 if (pc_sftp_wr_pos(&w) + el > PC_SFTP_RESP_CAP - 8) // would not fit this NAME response
261 {
262 if (count == 0) // first entry too big for an empty response - emit it anyway (best effort)
263 {
264 pc_sftp_wr_bytes(&w, ent, el);
265 count++;
266 }
267 else // stash it for the next READDIR
268 {
269 memcpy(H->pend, ent, el);
270 H->pend_len = (uint16_t)el;
271 H->has_pending = true;
272 }
273 break;
274 }
275 pc_sftp_wr_bytes(&w, ent, el);
276 count++;
277 }
278
279 if (count == 0) // nothing to return -> end of directory
280 {
281 send_status(s, id, PC_SSH_FX_EOF, "");
282 return;
283 }
284 pc_sftp_wr_patch_u32(&w, count_at, count);
285 size_t n = pc_sftp_wr_finish(&w);
286 send_resp(s, n);
287}
288
289// --- one complete non-WRITE request ---------------------------------------------------------------
290void handle_packet(SftpSession *s, const uint8_t *buf, size_t total)
291{
292 SftpReader r;
293 pc_sftp_rd_init(&r, buf + 4, total - 4);
294 uint8_t type = pc_sftp_rd_u8(&r);
295
296 if (type == PC_SSH_FXP_INIT)
297 {
298 send_resp(s, pc_sftp_build_version(s_sftp.out, PC_SFTP_RESP_CAP));
299 return;
300 }
301
302 uint32_t id = pc_sftp_rd_u32(&r);
303 if (!r.ok)
304 {
305 return;
306 }
307
308 switch (type)
309 {
310 case PC_SSH_FXP_OPEN: {
311 const uint8_t *p = nullptr;
312 uint32_t pl = 0;
313 if (!pc_sftp_rd_string(&r, &p, &pl))
314 {
315 send_status(s, id, PC_SSH_FX_BAD_MESSAGE, "");
316 return;
317 }
318 uint32_t pflags = pc_sftp_rd_u32(&r);
319 SftpAttrs a;
320 pc_sftp_rd_attrs(&r, &a);
321 char disk[PC_SFTP_PATH_MAX];
322 if (resolve(p, pl, disk, sizeof(disk)) != 0)
323 {
324 send_status(s, id, PC_SSH_FX_PERMISSION_DENIED, "bad path");
325 return;
326 }
327 bool writing = (pflags & PC_SSH_FXF_WRITE) != 0;
328 const char *mode = writing ? ((pflags & PC_SSH_FXF_APPEND) ? "a" : "w") : "r";
329 fs::File f = s_sftp.fs->open(disk, mode);
330 if (!f)
331 {
332 send_status(s, id, open_fail_code(writing), "open failed");
333 return;
334 }
335 if (f.isDirectory())
336 {
337 f.close();
338 send_status(s, id, PC_SSH_FX_FAILURE, "is a directory");
339 return;
340 }
341 int hi = alloc_handle(s);
342 if (hi < 0)
343 {
344 f.close();
345 send_status(s, id, PC_SSH_FX_FAILURE, "too many open handles");
346 return;
347 }
348 s->handles[hi].used = true;
349 s->handles[hi].is_dir = false;
350 s->handles[hi].file = f;
351 s->handles[hi].readdir_done = false;
352 s->handles[hi].has_pending = false;
353 strncpy(s->handles[hi].path, disk, sizeof(s->handles[hi].path) - 1);
354 s->handles[hi].path[sizeof(s->handles[hi].path) - 1] = '\0';
355 send_handle(s, id, hi);
356 return;
357 }
358 case PC_SSH_FXP_CLOSE: {
359 const uint8_t *h = nullptr;
360 uint32_t hl = 0;
361 pc_sftp_rd_string(&r, &h, &hl);
362 int hi = handle_index(s, h, hl);
363 if (hi < 0)
364 {
365 send_status(s, id, PC_SSH_FX_FAILURE, "bad handle");
366 return;
367 }
368 free_handle(s, hi);
369 send_status(s, id, PC_SSH_FX_OK, "");
370 return;
371 }
372 case PC_SSH_FXP_READ: {
373 const uint8_t *h = nullptr;
374 uint32_t hl = 0;
375 pc_sftp_rd_string(&r, &h, &hl);
376 uint64_t off = pc_sftp_rd_u64(&r);
377 uint32_t rlen = pc_sftp_rd_u32(&r);
378 int hi = handle_index(s, h, hl);
379 if (!r.ok || hi < 0 || s->handles[hi].is_dir)
380 {
381 send_status(s, id, PC_SSH_FX_FAILURE, "bad handle");
382 return;
383 }
384 fs::File &f = s->handles[hi].file;
385 f.seek((uint32_t)off);
386 uint32_t want = rlen < PC_SFTP_MAX_READ ? rlen : PC_SFTP_MAX_READ;
387 size_t got = f.read(s_sftp.rbuf, want);
388 if (got == 0)
389 {
390 send_status(s, id, PC_SSH_FX_EOF, "");
391 return;
392 }
393 send_resp(s, pc_sftp_build_data(id, s_sftp.rbuf, (uint32_t)got, s_sftp.out, PC_SFTP_RESP_CAP));
394 return;
395 }
396 case PC_SSH_FXP_OPENDIR: {
397 const uint8_t *p = nullptr;
398 uint32_t pl = 0;
399 pc_sftp_rd_string(&r, &p, &pl);
400 char disk[PC_SFTP_PATH_MAX];
401 if (!r.ok || resolve(p, pl, disk, sizeof(disk)) != 0)
402 {
403 send_status(s, id, PC_SSH_FX_PERMISSION_DENIED, "bad path");
404 return;
405 }
406 fs::File d = s_sftp.fs->open(disk, "r");
407 if (!d || !d.isDirectory())
408 {
409 if (d)
410 {
411 d.close();
412 }
413 send_status(s, id, PC_SSH_FX_NO_SUCH_FILE, "not a directory");
414 return;
415 }
416 int hi = alloc_handle(s);
417 if (hi < 0)
418 {
419 d.close();
420 send_status(s, id, PC_SSH_FX_FAILURE, "too many open handles");
421 return;
422 }
423 s->handles[hi].used = true;
424 s->handles[hi].is_dir = true;
425 s->handles[hi].file = d;
426 s->handles[hi].readdir_done = false;
427 s->handles[hi].has_pending = false;
428 strncpy(s->handles[hi].path, disk, sizeof(s->handles[hi].path) - 1);
429 s->handles[hi].path[sizeof(s->handles[hi].path) - 1] = '\0';
430 send_handle(s, id, hi);
431 return;
432 }
433 case PC_SSH_FXP_READDIR: {
434 const uint8_t *h = nullptr;
435 uint32_t hl = 0;
436 pc_sftp_rd_string(&r, &h, &hl);
437 int hi = handle_index(s, h, hl);
438 if (hi < 0 || !s->handles[hi].is_dir)
439 {
440 send_status(s, id, PC_SSH_FX_FAILURE, "bad handle");
441 return;
442 }
443 do_readdir(s, id, &s->handles[hi]);
444 return;
445 }
446 case PC_SSH_FXP_STAT:
447 case PC_SSH_FXP_LSTAT: {
448 const uint8_t *p = nullptr;
449 uint32_t pl = 0;
450 pc_sftp_rd_string(&r, &p, &pl);
451 char disk[PC_SFTP_PATH_MAX];
452 if (!r.ok || resolve(p, pl, disk, sizeof(disk)) != 0)
453 {
454 send_status(s, id, PC_SSH_FX_PERMISSION_DENIED, "bad path");
455 return;
456 }
457 fs::File f = s_sftp.fs->open(disk, "r");
458 if (!f)
459 {
460 send_status(s, id, PC_SSH_FX_NO_SUCH_FILE, "");
461 return;
462 }
463 SftpAttrs a;
464 attrs_from_file(f, &a);
465 f.close();
466 send_resp(s, pc_sftp_build_attrs(id, &a, s_sftp.out, PC_SFTP_RESP_CAP));
467 return;
468 }
469 case PC_SSH_FXP_FSTAT: {
470 const uint8_t *h = nullptr;
471 uint32_t hl = 0;
472 pc_sftp_rd_string(&r, &h, &hl);
473 int hi = handle_index(s, h, hl);
474 if (hi < 0)
475 {
476 send_status(s, id, PC_SSH_FX_FAILURE, "bad handle");
477 return;
478 }
479 SftpAttrs a;
480 attrs_from_file(s->handles[hi].file, &a);
481 send_resp(s, pc_sftp_build_attrs(id, &a, s_sftp.out, PC_SFTP_RESP_CAP));
482 return;
483 }
484 case PC_SSH_FXP_REMOVE: {
485 const uint8_t *p = nullptr;
486 uint32_t pl = 0;
487 pc_sftp_rd_string(&r, &p, &pl);
488 char disk[PC_SFTP_PATH_MAX];
489 if (!r.ok || resolve(p, pl, disk, sizeof(disk)) != 0)
490 {
491 send_status(s, id, PC_SSH_FX_PERMISSION_DENIED, "bad path");
492 return;
493 }
494 send_status(s, id, s_sftp.fs->remove(disk) ? PC_SSH_FX_OK : PC_SSH_FX_FAILURE, "");
495 return;
496 }
497 case PC_SSH_FXP_MKDIR: {
498 const uint8_t *p = nullptr;
499 uint32_t pl = 0;
500 pc_sftp_rd_string(&r, &p, &pl);
501 char disk[PC_SFTP_PATH_MAX];
502 if (!r.ok || resolve(p, pl, disk, sizeof(disk)) != 0)
503 {
504 send_status(s, id, PC_SSH_FX_PERMISSION_DENIED, "bad path");
505 return;
506 }
507 send_status(s, id, s_sftp.fs->mkdir(disk) ? PC_SSH_FX_OK : PC_SSH_FX_FAILURE, "");
508 return;
509 }
510 case PC_SSH_FXP_RMDIR: {
511 const uint8_t *p = nullptr;
512 uint32_t pl = 0;
513 pc_sftp_rd_string(&r, &p, &pl);
514 char disk[PC_SFTP_PATH_MAX];
515 if (!r.ok || resolve(p, pl, disk, sizeof(disk)) != 0)
516 {
517 send_status(s, id, PC_SSH_FX_PERMISSION_DENIED, "bad path");
518 return;
519 }
520 send_status(s, id, s_sftp.fs->rmdir(disk) ? PC_SSH_FX_OK : PC_SSH_FX_FAILURE, "");
521 return;
522 }
523 case PC_SSH_FXP_RENAME: {
524 const uint8_t *op = nullptr;
525 uint32_t ol = 0;
526 const uint8_t *np = nullptr;
527 uint32_t nl = 0;
528 pc_sftp_rd_string(&r, &op, &ol);
529 pc_sftp_rd_string(&r, &np, &nl);
530 char od[PC_SFTP_PATH_MAX];
531 char nd[PC_SFTP_PATH_MAX];
532 if (!r.ok || resolve(op, ol, od, sizeof(od)) != 0 || resolve(np, nl, nd, sizeof(nd)) != 0)
533 {
534 send_status(s, id, PC_SSH_FX_PERMISSION_DENIED, "bad path");
535 return;
536 }
537 send_status(s, id, s_sftp.fs->rename(od, nd) ? PC_SSH_FX_OK : PC_SSH_FX_FAILURE, "");
538 return;
539 }
540 case PC_SSH_FXP_REALPATH: {
541 const uint8_t *p = nullptr;
542 uint32_t pl = 0;
543 pc_sftp_rd_string(&r, &p, &pl);
544 if (!r.ok)
545 {
546 send_status(s, id, PC_SSH_FX_BAD_MESSAGE, "");
547 return;
548 }
549 char req[PC_SFTP_PATH_MAX];
550 if (pl >= sizeof(req))
551 {
552 send_status(s, id, PC_SSH_FX_FAILURE, "path too long");
553 return;
554 }
555 memcpy(req, p, pl);
556 req[pl] = '\0';
557 if (strstr(req, ".."))
558 {
559 send_status(s, id, PC_SSH_FX_PERMISSION_DENIED, "traversal");
560 return;
561 }
562 char cpath[PC_SFTP_PATH_MAX + 1];
563 if (pl == 0 || (pl == 1 && req[0] == '.'))
564 {
565 pc_sb sb_cpath = {cpath, sizeof(cpath), 0, true};
566 pc_sb_put(&sb_cpath, "/");
567 if (pc_sb_finish(&sb_cpath) == 0)
568 {
569 cpath[0] = '\0';
570 }
571 }
572 else if (req[0] == '/')
573 {
574 pc_sb sb_cpath2 = {cpath, sizeof(cpath), 0, true};
575 pc_sb_put(&sb_cpath2, req);
576 if (pc_sb_finish(&sb_cpath2) == 0)
577 {
578 cpath[0] = '\0';
579 }
580 }
581 else
582 {
583 pc_sb sb_cpath3 = {cpath, sizeof(cpath), 0, true};
584 pc_sb_put(&sb_cpath3, "/");
585 pc_sb_put(&sb_cpath3, req);
586 if (pc_sb_finish(&sb_cpath3) == 0)
587 {
588 cpath[0] = '\0';
589 }
590 }
591 SftpAttrs a;
592 a.flags = PC_SSH_FILEXFER_ATTR_PERMS;
593 a.permissions = PC_SFTP_S_IFDIR | 0755;
594 a.size = 0;
595 a.atime = a.mtime = 0;
596 char ln[64];
597 pc_sftp_format_longname(true, a.permissions, 0, 0, cpath, ln, sizeof(ln));
598 send_resp(s, pc_sftp_build_name1(id, cpath, ln, &a, s_sftp.out, PC_SFTP_RESP_CAP));
599 return;
600 }
601 case PC_SSH_FXP_SETSTAT:
602 case PC_SSH_FXP_FSETSTAT:
603 // We do not implement chmod/chown/truncate-via-setstat; accept it (OK) so `put` does not fail on the
604 // trailing FSETSTAT that sets mode/mtime - the values are simply not applied.
605 send_status(s, id, PC_SSH_FX_OK, "");
606 return;
607 default:
608 send_status(s, id, PC_SSH_FX_OP_UNSUPPORTED, "unsupported");
609 return;
610 }
611}
612
613// --- framing loop ---------------------------------------------------------------------------------
614// Consume complete packets from the accumulator. A WRITE switches to streaming mode. @return false to tear the
615// channel down (malformed / oversized non-WRITE packet).
616bool process_acc(SftpSession *s)
617{
618 for (;;)
619 {
620 if (s->acc_len < 5)
621 {
622 return true; // need the length prefix + the type byte
623 }
624 uint32_t plen = ((uint32_t)s->acc[0] << 24) | ((uint32_t)s->acc[1] << 16) | ((uint32_t)s->acc[2] << 8) |
625 (uint32_t)s->acc[3];
626 if (plen == 0)
627 {
628 return false; // malformed
629 }
630 size_t total = (size_t)plen + 4;
631 uint8_t type = s->acc[4];
632
633 if (type == PC_SSH_FXP_WRITE)
634 {
635 SftpReader r;
636 pc_sftp_rd_init(&r, s->acc + 4, s->acc_len - 4);
637 pc_sftp_rd_u8(&r); // type
638 uint32_t id = pc_sftp_rd_u32(&r);
639 const uint8_t *h = nullptr;
640 uint32_t hl = 0;
641 if (!pc_sftp_rd_string(&r, &h, &hl))
642 {
643 return true; // the handle has not fully arrived - wait
644 }
645 uint64_t off = pc_sftp_rd_u64(&r);
646 uint32_t datalen = pc_sftp_rd_u32(&r);
647 if (!r.ok)
648 {
649 return true; // the WRITE header has not fully arrived - wait
650 }
651
652 int hi = handle_index(s, h, hl);
653 s->wr_id = id;
654 s->wr_remaining = datalen;
655 s->wr_off = off;
656 s->wr_handle = hi;
657 s->wr_err = (hi < 0 || s->handles[hi].is_dir);
658 if (!s->wr_err)
659 {
660 s->handles[hi].file.seek((uint32_t)off);
661 }
662 s->writing = true;
663
664 size_t hdr = 4 + r.off; // header bytes of this packet (before the data payload)
665 size_t have = s->acc_len - hdr;
666 size_t chunk = have < datalen ? have : datalen;
667 write_stream_bytes(s, s->acc + hdr, chunk);
668 size_t consumed = hdr + chunk;
669 memmove(s->acc, s->acc + consumed, s->acc_len - consumed);
670 s->acc_len -= (uint16_t)consumed;
671 if (s->wr_remaining == 0)
672 {
673 finish_write(s); // all data was already in the accumulator
674 }
675 if (s->writing)
676 {
677 return true; // still streaming - the rest arrives as raw channel data
678 }
679 continue; // write finished within the accumulator; process the next packet
680 }
681
682 if (total > sizeof(s->acc))
683 {
684 return false; // a non-WRITE packet larger than the buffer -> drop the channel
685 }
686 if (s->acc_len < total)
687 {
688 return true; // wait for the rest
689 }
690 handle_packet(s, s->acc, total);
691 memmove(s->acc, s->acc + total, s->acc_len - total);
692 s->acc_len -= (uint16_t)total;
693 }
694}
695
696// --- channel callbacks ----------------------------------------------------------------------------
697void pc_sftp_on_open(uint8_t slot, uint32_t channel)
698{
699 if (slot >= MAX_SSH_CONNS)
700 {
701 return;
702 }
703 SftpSession *s = &s_sftp.sess[slot];
704 free_all_handles(s); // clean any handles lingering from a prior session on this slot
705 s->active = true;
706 s->slot = slot;
707 s->channel = channel;
708 s->acc_len = 0;
709 s->writing = false;
710 s->wr_remaining = 0;
711 s->wr_handle = -1;
712 // The SFTP VERSION reply is sent when the client's INIT packet arrives.
713}
714
715void pc_sftp_on_data(uint8_t slot, uint32_t channel, const uint8_t *data, size_t len)
716{
717 if (slot >= MAX_SSH_CONNS)
718 {
719 return;
720 }
721 SftpSession *s = &s_sftp.sess[slot];
722 if (!s->active || s->channel != channel)
723 {
724 return;
725 }
726 while (len > 0)
727 {
728 if (s->writing)
729 {
730 size_t take = len < s->wr_remaining ? len : s->wr_remaining;
731 write_stream_bytes(s, data, take);
732 data += take;
733 len -= take;
734 if (s->wr_remaining == 0)
735 {
736 finish_write(s);
737 }
738 continue;
739 }
740 size_t space = sizeof(s->acc) - s->acc_len;
741 if (space == 0)
742 {
743 pc_ssh_conn_close_channel(slot, s->channel); // a non-WRITE packet too big to buffer
744 s->active = false;
745 return;
746 }
747 size_t take = len < space ? len : space;
748 memcpy(s->acc + s->acc_len, data, take);
749 s->acc_len += (uint16_t)take;
750 data += take;
751 len -= take;
752 if (!process_acc(s))
753 {
754 pc_ssh_conn_close_channel(slot, s->channel);
755 s->active = false;
756 return;
757 }
758 }
759}
760} // namespace
761
762// --- public API -----------------------------------------------------------------------------------
763void pc_ssh_sftp_begin(fs::FS &fs, const char *root)
764{
765 s_sftp.fs = &fs;
766 s_sftp.root = (root && root[0]) ? root : "/";
767 for (int i = 0; i < MAX_SSH_CONNS; i++)
768 {
769 s_sftp.sess[i].active = false;
770 free_all_handles(&s_sftp.sess[i]);
771 }
772 if (!s_sftp.registered)
773 {
774 pc_ssh_channel_set_sftp_open_cb(pc_sftp_on_open);
775 pc_ssh_channel_set_sftp_data_cb(pc_sftp_on_data);
776 s_sftp.registered = true;
777 }
778}
779
780#endif // PC_ENABLE_SSH_SFTP
#define MAX_SSH_CONNS
Definition c2_defaults.h:85
Shared filesystem path helpers for the file-transfer servers (SFTP / SCP, and the pattern the static ...
int fs_path_resolve(const char *root, const char *sub, char *out, size_t cap)
Resolve a mount root + a request sub path to an on-disk path in out: reject any .....
Definition fs_path.h:58
#define PC_SFTP_MAX_READ
Largest PC_SSH_FXP_DATA payload returned for one READ (a short read - the client re-requests)....
#define PC_SFTP_PATH_MAX
Largest absolute path the SFTP/SCP server resolves (mount root + request path).
#define PC_SFTP_PKT_BUF
SFTP packet-assembly buffer per SFTP channel (bytes); bounds one non-streamed request/response.
#define PC_SFTP_MAX_HANDLES
Max concurrent open SFTP handles (files + dirs) per SSH connection.
#define SSH_PKT_BUF_SIZE
Packet assembly buffer per SSH connection (bytes).
SFTP protocol v3 wire codec (SSH_FXP_*, draft-ietf-secsh-filexfer-02) - the pure, host-testable half ...
SSH connection protocol - multiplexed "session" channels (RFC 4254).
int pc_ssh_conn_send(uint8_t ssh_slot, uint32_t channel, const uint8_t *data, size_t len)
Send application data to the client over an SSH channel.
Definition ssh_conn.cpp:117
int pc_ssh_conn_close_channel(uint8_t ssh_slot, uint32_t channel)
Close an SSH channel from the server side: frame CHANNEL_EOF and CHANNEL_CLOSE as two binary packets ...
Definition ssh_conn.cpp:156
Glue between the TCP transport (conn_pool) and the SSH protocol stack.
SFTP server subsystem - the fs::FS binding (PC_ENABLE_SSH_SFTP).
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30