ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ssh_scp.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_scp.cpp
6 * @brief SCP server - fs::FS binding (SINK direction). See ssh_scp.h.
7 *
8 * Drives the rcp SINK protocol over an SSH `exec "scp -t <path>"` channel: send a ready ack, read the client's
9 * `C<mode> <size> <name>` control line, ack it, stream <size> data bytes straight to an fs::FS file, read the
10 * trailing end-of-record byte, and send the final ack. One file per transfer (no -r); the SOURCE direction
11 * (`scp -f`) replies "use sftp get". Every path is checked for `..` traversal before opening the file.
12 */
13
14#include "server/ssh_scp.h"
15#include "shared_primitives/strbuf.h" // pc_sb frame builder
16
17#if PC_ENABLE_SSH_SCP
18
21#include "server/fs_path.h"
23#include <string.h>
24
25namespace
26{
27enum class ScpSt : uint8_t
28{
29 PC_NONE,
30 WAIT_CLINE, ///< reading the C<mode> <size> <name> control line
31 RECV, ///< streaming file data to disk
32 WAIT_END ///< the file's bytes are in; awaiting the end-of-record byte
33};
34
35struct ScpConn
36{
37 bool active;
38 uint8_t slot;
39 uint32_t channel;
40 ScpSt st;
41 char dest[PC_SFTP_PATH_MAX]; ///< the -t target (a file, or a dir if it ends with '/')
42 bool dest_is_dir;
43 fs::File file;
44 uint64_t remaining; ///< data bytes still to receive
45 bool err;
46 uint16_t cl_len; ///< control-line accumulator length
47 char cl[PC_SFTP_PATH_MAX + 64];
48};
49
50struct SshScpCtx
51{
52 fs::FS *fs = nullptr;
53 const char *root = "/";
54 bool registered = false;
55 ScpConn conns[MAX_SSH_CONNS];
56};
57SshScpCtx s_scp;
58
59void ack(ScpConn *c, uint8_t byte)
60{
61 pc_ssh_conn_send(c->slot, c->channel, &byte, 1);
62}
63void err_ack(ScpConn *c, const char *msg)
64{
65 uint8_t buf[96];
66 buf[0] = PC_SCP_ACK_ERROR;
67 size_t ml = strnlen(msg, sizeof(buf) - 3);
68 memcpy(buf + 1, msg, ml);
69 buf[1 + ml] = '\n';
70 pc_ssh_conn_send(c->slot, c->channel, buf, 2 + ml);
71}
72void pc_scp_end(ScpConn *c)
73{
74 if (c->file)
75 {
76 c->file.close();
77 }
78 c->file = fs::File();
79 c->active = false;
80 pc_ssh_conn_close_channel(c->slot, c->channel);
81}
82
83void pc_scp_on_open(uint8_t slot, uint32_t channel, const char *cmd, size_t cmd_len)
84{
85 if (slot >= MAX_SSH_CONNS)
86 {
87 return;
88 }
89 ScpConn *c = &s_scp.conns[slot];
90 if (c->file)
91 {
92 c->file.close();
93 }
94 c->file = fs::File();
95 c->active = true;
96 c->slot = slot;
97 c->channel = channel;
98 c->err = false;
99 c->cl_len = 0;
100
101 char path[PC_SFTP_PATH_MAX];
102 ScpMode mode = pc_scp_parse_cmd(cmd, cmd_len, path, sizeof(path));
103 if (mode == ScpMode::SINK)
104 {
105 size_t pl = strnlen(path, sizeof(path));
106 c->dest_is_dir = (pl > 0 && path[pl - 1] == '/');
107 strncpy(c->dest, path, sizeof(c->dest) - 1);
108 c->dest[sizeof(c->dest) - 1] = '\0';
109 c->st = ScpSt::WAIT_CLINE;
110 ack(c, PC_SCP_ACK_OK); // ready for the control line
111 }
112 else if (mode == ScpMode::SOURCE)
113 {
114 err_ack(c, "scp download not supported; use sftp get");
115 pc_scp_end(c);
116 }
117 else
118 {
119 err_ack(c, "unsupported scp command");
120 pc_scp_end(c);
121 }
122}
123
124// Resolve the on-disk destination for a received file named @p name.
125bool pc_scp_resolve_dest(ScpConn *c, const char *name, char *out, size_t cap)
126{
127 char sub[PC_SFTP_PATH_MAX + 96];
128 if (c->dest_is_dir)
129 {
130 pc_sb sb_sub = {sub, sizeof(sub), 0, true};
131 pc_sb_put(&sb_sub, c->dest);
132 pc_sb_put(&sb_sub, name);
133 if (pc_sb_finish(&sb_sub) == 0)
134 {
135 sub[0] = '\0'; // c->dest ends with '/'
136 }
137 }
138 else
139 {
140 pc_sb sb_sub2 = {sub, sizeof(sub), 0, true};
141 pc_sb_put(&sb_sub2, c->dest);
142 if (pc_sb_finish(&sb_sub2) == 0)
143 {
144 sub[0] = '\0';
145 }
146 }
147 return fs_path_resolve(s_scp.root, sub, out, cap) == 0;
148}
149
150void pc_scp_on_data(uint8_t slot, uint32_t channel, const uint8_t *data, size_t len)
151{
152 if (slot >= MAX_SSH_CONNS)
153 {
154 return;
155 }
156 ScpConn *c = &s_scp.conns[slot];
157 if (!c->active || c->channel != channel)
158 {
159 return;
160 }
161
162 while (len > 0)
163 {
164 if (c->st == ScpSt::WAIT_CLINE)
165 {
166 bool complete = false;
167 while (len > 0)
168 {
169 char ch = (char)data[0];
170 data++;
171 len--;
172 if (ch == '\n')
173 {
174 complete = true;
175 break;
176 }
177 if (c->cl_len < sizeof(c->cl) - 1)
178 {
179 c->cl[c->cl_len++] = ch;
180 }
181 }
182 if (!complete)
183 {
184 return; // the whole control line has not arrived yet
185 }
186 c->cl[c->cl_len] = '\0';
187
188 uint32_t mode = 0;
189 uint64_t size = 0;
190 char name[PC_SFTP_PATH_MAX];
191 if (!pc_scp_parse_cline(c->cl, c->cl_len, &mode, &size, name, sizeof(name)))
192 {
193 err_ack(c, "unsupported scp record"); // e.g. a D/E directory record (no -r support)
194 pc_scp_end(c);
195 return;
196 }
197 char disk[PC_SFTP_PATH_MAX];
198 if (!pc_scp_resolve_dest(c, name, disk, sizeof(disk)))
199 {
200 err_ack(c, "bad path");
201 pc_scp_end(c);
202 return;
203 }
204 c->file = s_scp.fs->open(disk, "w");
205 if (!c->file)
206 {
207 err_ack(c, "cannot create file");
208 pc_scp_end(c);
209 return;
210 }
211 c->remaining = size;
212 c->st = (size == 0) ? ScpSt::WAIT_END : ScpSt::RECV;
213 c->cl_len = 0;
214 ack(c, PC_SCP_ACK_OK); // proceed with the data
215 continue;
216 }
217 if (c->st == ScpSt::RECV)
218 {
219 size_t take = (len < c->remaining) ? len : (size_t)c->remaining;
220 if (c->file.write(data, take) != take)
221 {
222 c->err = true;
223 }
224 data += take;
225 len -= take;
226 c->remaining -= take;
227 if (c->remaining == 0)
228 {
229 c->st = ScpSt::WAIT_END;
230 }
231 continue;
232 }
233 if (c->st == ScpSt::WAIT_END)
234 {
235 data++; // consume the end-of-record byte (0)
236 len--;
237 if (c->file)
238 {
239 c->file.close();
240 }
241 c->file = fs::File();
242 if (c->err)
243 {
244 err_ack(c, "write error");
245 }
246 else
247 {
248 ack(c, PC_SCP_ACK_OK);
249 }
250 pc_scp_end(c);
251 return;
252 }
253 return; // PC_NONE / unexpected
254 }
255}
256} // namespace
257
258void pc_ssh_scp_begin(fs::FS &fs, const char *root)
259{
260 s_scp.fs = &fs;
261 s_scp.root = (root && root[0]) ? root : "/";
262 for (int i = 0; i < MAX_SSH_CONNS; i++)
263 {
264 s_scp.conns[i].active = false;
265 if (s_scp.conns[i].file)
266 {
267 s_scp.conns[i].file.close();
268 }
269 s_scp.conns[i].file = fs::File();
270 }
271 if (!s_scp.registered)
272 {
273 pc_ssh_channel_set_scp_open_cb(pc_scp_on_open);
274 pc_ssh_channel_set_scp_data_cb(pc_scp_on_data);
275 s_scp.registered = true;
276 }
277}
278
279#endif // PC_ENABLE_SSH_SCP
#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_PATH_MAX
Largest absolute path the SFTP/SCP server resolves (mount root + request path).
SCP (RCP) protocol wire codec - the pure, host-testable half of the SCP-over-SSH server (PC_ENABLE_SS...
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.
SCP server - the fs::FS binding (PC_ENABLE_SSH_SCP).
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