29#define PC_SFTP_RESP_CAP (SSH_PKT_BUF_SIZE - 16)
31#define PC_SFTP_ENTRY_MAX (PC_SFTP_PATH_MAX + 320)
42 uint8_t pend[PC_SFTP_ENTRY_MAX];
56 uint32_t wr_remaining;
65 const char *root =
"/";
66 bool registered =
false;
74void free_handle(SftpSession *s,
int h)
80 if (s->handles[h].file)
82 s->handles[h].file.close();
84 s->handles[h].file = fs::File();
85 s->handles[h].used =
false;
86 s->handles[h].has_pending =
false;
88void free_all_handles(SftpSession *s)
95int alloc_handle(SftpSession *s)
99 if (!s->handles[i].used)
107int handle_index(SftpSession *s,
const uint8_t *h, uint32_t hl)
113 uint32_t idx = ((uint32_t)h[0] << 24) | ((uint32_t)h[1] << 16) | ((uint32_t)h[2] << 8) | (uint32_t)h[3];
122const char *base_name(
const char *name)
124 const char *slash = strrchr(name,
'/');
125 return slash ? slash + 1 : name;
128void attrs_from_file(fs::File &f, SftpAttrs *a)
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();
140int resolve(
const uint8_t *path, uint32_t plen,
char *out,
size_t cap)
143 if (plen >=
sizeof(req))
147 memcpy(req, path, plen);
149 const char *sub = (plen == 0 || (plen == 1 && req[0] ==
'.')) ?
"/" : req;
153void send_resp(SftpSession *s,
size_t n)
160void send_status(SftpSession *s, uint32_t
id, uint32_t code,
const char *msg)
162 send_resp(s, pc_sftp_build_status(
id, code, msg, s_sftp.out, PC_SFTP_RESP_CAP));
164void send_handle(SftpSession *s, uint32_t
id,
int hi)
166 uint8_t hb[4] = {(uint8_t)((uint32_t)hi >> 24), (uint8_t)((uint32_t)hi >> 16), (uint8_t)((uint32_t)hi >> 8),
168 send_resp(s, pc_sftp_build_handle(
id, hb, 4, s_sftp.out, PC_SFTP_RESP_CAP));
172uint32_t open_fail_code(
bool writing)
174 return writing ? PC_SSH_FX_FAILURE : PC_SSH_FX_NO_SUCH_FILE;
178void write_stream_bytes(SftpSession *s,
const uint8_t *data,
size_t n)
184 if (!s->wr_err && s->wr_handle >= 0)
186 fs::File &f = s->handles[s->wr_handle].file;
187 if (f.write(data, n) != n)
193 s->wr_remaining -= (uint32_t)n;
195void finish_write(SftpSession *s)
197 send_status(s, s->wr_id, s->wr_err ? PC_SSH_FX_FAILURE : PC_SSH_FX_OK, s->wr_err ?
"write failed" :
"");
203size_t build_entry(fs::File &c, uint8_t *ent,
size_t cap)
205 const char *base = base_name(c.name());
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));
211 pc_sftp_wr_init(&w, ent, cap);
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);
219 size_t el = w.off - 4;
220 memmove(ent, ent + 4, el);
224void do_readdir(SftpSession *s, uint32_t
id, SftpHandle *H)
226 if (H->readdir_done && !H->has_pending)
228 send_status(s,
id, PC_SSH_FX_EOF,
"");
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);
241 pc_sftp_wr_bytes(&w, H->pend, H->pend_len);
242 H->has_pending =
false;
245 while (!H->readdir_done)
247 fs::File c = H->file.openNextFile();
250 H->readdir_done =
true;
253 uint8_t ent[PC_SFTP_ENTRY_MAX];
254 size_t el = build_entry(c, ent,
sizeof(ent));
260 if (pc_sftp_wr_pos(&w) + el > PC_SFTP_RESP_CAP - 8)
264 pc_sftp_wr_bytes(&w, ent, el);
269 memcpy(H->pend, ent, el);
270 H->pend_len = (uint16_t)el;
271 H->has_pending =
true;
275 pc_sftp_wr_bytes(&w, ent, el);
281 send_status(s,
id, PC_SSH_FX_EOF,
"");
284 pc_sftp_wr_patch_u32(&w, count_at, count);
285 size_t n = pc_sftp_wr_finish(&w);
290void handle_packet(SftpSession *s,
const uint8_t *buf,
size_t total)
293 pc_sftp_rd_init(&r, buf + 4, total - 4);
294 uint8_t type = pc_sftp_rd_u8(&r);
296 if (type == PC_SSH_FXP_INIT)
298 send_resp(s, pc_sftp_build_version(s_sftp.out, PC_SFTP_RESP_CAP));
302 uint32_t
id = pc_sftp_rd_u32(&r);
310 case PC_SSH_FXP_OPEN: {
311 const uint8_t *p =
nullptr;
313 if (!pc_sftp_rd_string(&r, &p, &pl))
315 send_status(s,
id, PC_SSH_FX_BAD_MESSAGE,
"");
318 uint32_t pflags = pc_sftp_rd_u32(&r);
320 pc_sftp_rd_attrs(&r, &a);
322 if (resolve(p, pl, disk,
sizeof(disk)) != 0)
324 send_status(s,
id, PC_SSH_FX_PERMISSION_DENIED,
"bad path");
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);
332 send_status(s,
id, open_fail_code(writing),
"open failed");
338 send_status(s,
id, PC_SSH_FX_FAILURE,
"is a directory");
341 int hi = alloc_handle(s);
345 send_status(s,
id, PC_SSH_FX_FAILURE,
"too many open handles");
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);
358 case PC_SSH_FXP_CLOSE: {
359 const uint8_t *h =
nullptr;
361 pc_sftp_rd_string(&r, &h, &hl);
362 int hi = handle_index(s, h, hl);
365 send_status(s,
id, PC_SSH_FX_FAILURE,
"bad handle");
369 send_status(s,
id, PC_SSH_FX_OK,
"");
372 case PC_SSH_FXP_READ: {
373 const uint8_t *h =
nullptr;
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)
381 send_status(s,
id, PC_SSH_FX_FAILURE,
"bad handle");
384 fs::File &f = s->handles[hi].file;
385 f.seek((uint32_t)off);
387 size_t got = f.read(s_sftp.rbuf, want);
390 send_status(s,
id, PC_SSH_FX_EOF,
"");
393 send_resp(s, pc_sftp_build_data(
id, s_sftp.rbuf, (uint32_t)got, s_sftp.out, PC_SFTP_RESP_CAP));
396 case PC_SSH_FXP_OPENDIR: {
397 const uint8_t *p =
nullptr;
399 pc_sftp_rd_string(&r, &p, &pl);
401 if (!r.ok || resolve(p, pl, disk,
sizeof(disk)) != 0)
403 send_status(s,
id, PC_SSH_FX_PERMISSION_DENIED,
"bad path");
406 fs::File d = s_sftp.fs->open(disk,
"r");
407 if (!d || !d.isDirectory())
413 send_status(s,
id, PC_SSH_FX_NO_SUCH_FILE,
"not a directory");
416 int hi = alloc_handle(s);
420 send_status(s,
id, PC_SSH_FX_FAILURE,
"too many open handles");
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);
433 case PC_SSH_FXP_READDIR: {
434 const uint8_t *h =
nullptr;
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)
440 send_status(s,
id, PC_SSH_FX_FAILURE,
"bad handle");
443 do_readdir(s,
id, &s->handles[hi]);
446 case PC_SSH_FXP_STAT:
447 case PC_SSH_FXP_LSTAT: {
448 const uint8_t *p =
nullptr;
450 pc_sftp_rd_string(&r, &p, &pl);
452 if (!r.ok || resolve(p, pl, disk,
sizeof(disk)) != 0)
454 send_status(s,
id, PC_SSH_FX_PERMISSION_DENIED,
"bad path");
457 fs::File f = s_sftp.fs->open(disk,
"r");
460 send_status(s,
id, PC_SSH_FX_NO_SUCH_FILE,
"");
464 attrs_from_file(f, &a);
466 send_resp(s, pc_sftp_build_attrs(
id, &a, s_sftp.out, PC_SFTP_RESP_CAP));
469 case PC_SSH_FXP_FSTAT: {
470 const uint8_t *h =
nullptr;
472 pc_sftp_rd_string(&r, &h, &hl);
473 int hi = handle_index(s, h, hl);
476 send_status(s,
id, PC_SSH_FX_FAILURE,
"bad handle");
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));
484 case PC_SSH_FXP_REMOVE: {
485 const uint8_t *p =
nullptr;
487 pc_sftp_rd_string(&r, &p, &pl);
489 if (!r.ok || resolve(p, pl, disk,
sizeof(disk)) != 0)
491 send_status(s,
id, PC_SSH_FX_PERMISSION_DENIED,
"bad path");
494 send_status(s,
id, s_sftp.fs->remove(disk) ? PC_SSH_FX_OK : PC_SSH_FX_FAILURE,
"");
497 case PC_SSH_FXP_MKDIR: {
498 const uint8_t *p =
nullptr;
500 pc_sftp_rd_string(&r, &p, &pl);
502 if (!r.ok || resolve(p, pl, disk,
sizeof(disk)) != 0)
504 send_status(s,
id, PC_SSH_FX_PERMISSION_DENIED,
"bad path");
507 send_status(s,
id, s_sftp.fs->mkdir(disk) ? PC_SSH_FX_OK : PC_SSH_FX_FAILURE,
"");
510 case PC_SSH_FXP_RMDIR: {
511 const uint8_t *p =
nullptr;
513 pc_sftp_rd_string(&r, &p, &pl);
515 if (!r.ok || resolve(p, pl, disk,
sizeof(disk)) != 0)
517 send_status(s,
id, PC_SSH_FX_PERMISSION_DENIED,
"bad path");
520 send_status(s,
id, s_sftp.fs->rmdir(disk) ? PC_SSH_FX_OK : PC_SSH_FX_FAILURE,
"");
523 case PC_SSH_FXP_RENAME: {
524 const uint8_t *op =
nullptr;
526 const uint8_t *np =
nullptr;
528 pc_sftp_rd_string(&r, &op, &ol);
529 pc_sftp_rd_string(&r, &np, &nl);
532 if (!r.ok || resolve(op, ol, od,
sizeof(od)) != 0 || resolve(np, nl, nd,
sizeof(nd)) != 0)
534 send_status(s,
id, PC_SSH_FX_PERMISSION_DENIED,
"bad path");
537 send_status(s,
id, s_sftp.fs->rename(od, nd) ? PC_SSH_FX_OK : PC_SSH_FX_FAILURE,
"");
540 case PC_SSH_FXP_REALPATH: {
541 const uint8_t *p =
nullptr;
543 pc_sftp_rd_string(&r, &p, &pl);
546 send_status(s,
id, PC_SSH_FX_BAD_MESSAGE,
"");
550 if (pl >=
sizeof(req))
552 send_status(s,
id, PC_SSH_FX_FAILURE,
"path too long");
557 if (strstr(req,
".."))
559 send_status(s,
id, PC_SSH_FX_PERMISSION_DENIED,
"traversal");
563 if (pl == 0 || (pl == 1 && req[0] ==
'.'))
565 pc_sb sb_cpath = {cpath,
sizeof(cpath), 0,
true};
572 else if (req[0] ==
'/')
574 pc_sb sb_cpath2 = {cpath,
sizeof(cpath), 0,
true};
583 pc_sb sb_cpath3 = {cpath,
sizeof(cpath), 0,
true};
592 a.flags = PC_SSH_FILEXFER_ATTR_PERMS;
593 a.permissions = PC_SFTP_S_IFDIR | 0755;
595 a.atime = a.mtime = 0;
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));
601 case PC_SSH_FXP_SETSTAT:
602 case PC_SSH_FXP_FSETSTAT:
605 send_status(s,
id, PC_SSH_FX_OK,
"");
608 send_status(s,
id, PC_SSH_FX_OP_UNSUPPORTED,
"unsupported");
616bool process_acc(SftpSession *s)
624 uint32_t plen = ((uint32_t)s->acc[0] << 24) | ((uint32_t)s->acc[1] << 16) | ((uint32_t)s->acc[2] << 8) |
630 size_t total = (size_t)plen + 4;
631 uint8_t type = s->acc[4];
633 if (type == PC_SSH_FXP_WRITE)
636 pc_sftp_rd_init(&r, s->acc + 4, s->acc_len - 4);
638 uint32_t
id = pc_sftp_rd_u32(&r);
639 const uint8_t *h =
nullptr;
641 if (!pc_sftp_rd_string(&r, &h, &hl))
645 uint64_t off = pc_sftp_rd_u64(&r);
646 uint32_t datalen = pc_sftp_rd_u32(&r);
652 int hi = handle_index(s, h, hl);
654 s->wr_remaining = datalen;
657 s->wr_err = (hi < 0 || s->handles[hi].is_dir);
660 s->handles[hi].file.seek((uint32_t)off);
664 size_t hdr = 4 + r.off;
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)
682 if (total >
sizeof(s->acc))
686 if (s->acc_len < total)
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;
697void pc_sftp_on_open(uint8_t slot, uint32_t channel)
703 SftpSession *s = &s_sftp.sess[slot];
707 s->channel = channel;
715void pc_sftp_on_data(uint8_t slot, uint32_t channel,
const uint8_t *data,
size_t len)
721 SftpSession *s = &s_sftp.sess[slot];
722 if (!s->active || s->channel != channel)
730 size_t take = len < s->wr_remaining ? len : s->wr_remaining;
731 write_stream_bytes(s, data, take);
734 if (s->wr_remaining == 0)
740 size_t space =
sizeof(s->acc) - s->acc_len;
747 size_t take = len < space ? len : space;
748 memcpy(s->acc + s->acc_len, data, take);
749 s->acc_len += (uint16_t)take;
763void pc_ssh_sftp_begin(fs::FS &fs,
const char *root)
766 s_sftp.root = (root && root[0]) ? root :
"/";
769 s_sftp.sess[i].active =
false;
770 free_all_handles(&s_sftp.sess[i]);
772 if (!s_sftp.registered)
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;
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 .....
#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.
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 ...
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.
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.
Bump-append target; ok latches false once an append would overflow cap.