ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ftp_session.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 ftp_session.cpp
6 * @brief Drive a real FTP control + data connection pair with the ftp.h codec (see ftp_session.h).
7 */
8
10#include "shared_primitives/strbuf.h" // pc_sb frame builder
11
12#if PC_ENABLE_FTP_SESSION
13
16#include "services/system/clock.h" // pc_millis, pcdelay
18#include <stdio.h>
19#include <string.h>
20
21/** @brief Owned session state. One transfer at a time; the buffers are too big for the stack. */
22struct FtpSessionCtx
23{
24 int ctrl; ///< control-connection id, or -1
25 int data; ///< data-connection id, or -1
26 char rx[PC_FTP_REPLY_BUF]; ///< control-reply accumulator
27 size_t rx_len; ///< bytes held in rx
28 size_t rx_consumed; ///< bytes of rx the last reply occupied, shifted out on the next read
29 char cmd[PC_FTP_CMD_MAX]; ///< command being built
30 uint8_t chunk[PC_FTP_CHUNK]; ///< payload staging
31};
32static FtpSessionCtx s_ftp = {-1, -1, {0}, 0, 0, {0}, {0}};
33
34// Log frames. Each is the shape of one message, fixed when the code was written, so the logger
35// builds it without parsing anything and a level that is compiled out costs the spec nothing.
36static const pc_field LOG_BUILD_FAILED[] = {{PC_FK_LIT, 0, 18, "ftp: cannot build "}, PC_STR, PC_END};
37static const pc_field LOG_SENT[] = {{PC_FK_LIT, 0, 5, "ftp> "}, PC_STR, PC_END};
38static const pc_field LOG_REPLY[] = {{PC_FK_LIT, 0, 5, "ftp< "}, PC_U32, PC_END};
39static const pc_field LOG_REPLY_TOO_BIG[] = {
40 {PC_FK_LIT, 0, 41, "ftp: reply larger than PC_FTP_REPLY_BUF ("}, PC_U32, {PC_FK_LIT, 0, 1, ")"}, PC_END};
41static const pc_field LOG_CTRL_CLOSED[] = {
42 {PC_FK_LIT, 0, 25, "ftp: control closed with "}, PC_U32, {PC_FK_LIT, 0, 15, " bytes buffered"}, PC_END};
43static const pc_field LOG_REPLY_TIMEOUT[] = {
44 {PC_FK_LIT, 0, 20, "ftp: reply timeout, "}, PC_U32, {PC_FK_LIT, 0, 15, " bytes buffered"}, PC_END};
45static const pc_field LOG_DATA_CONNECT_FAILED[] = {{PC_FK_LIT, 0, 21, "ftp: data connect to "},
46 PC_STR,
47 {PC_FK_LIT, 0, 1, ":"},
48 PC_U32,
49 {PC_FK_LIT, 0, 7, " failed"},
50 PC_END};
51static const pc_field LOG_CTRL_CONNECT_FAILED[] = {{PC_FK_LIT, 0, 24, "ftp: control connect to "},
52 PC_STR,
53 {PC_FK_LIT, 0, 1, ":"},
54 PC_U32,
55 {PC_FK_LIT, 0, 7, " failed"},
56 PC_END};
57
58// ---------------------------------------------------------------------------
59// Control channel
60// ---------------------------------------------------------------------------
61
62/** @brief Send one command line on the control connection. */
63static bool ftp_send(const char *verb, const char *arg)
64{
65 size_t n = pc_ftp_build_command(s_ftp.cmd, sizeof(s_ftp.cmd), verb, arg);
66 if (n == 0)
67 {
68 PC_LOGW(LOG_BUILD_FAILED, verb);
69 return false;
70 }
71 PC_LOGD(LOG_SENT, verb);
72 return pc_client_send(s_ftp.ctrl, s_ftp.cmd, n);
73}
74
75/**
76 * @brief Read until a complete reply is buffered.
77 *
78 * The reply text is left at the head of rx (length in @p rlen) so a caller can hand it straight to
79 * pc_ftp_parse_pasv / _epsv; it is shifted out at the start of the next call, which keeps any
80 * bytes the server pipelined behind it.
81 */
82static bool ftp_await(int *code, size_t *rlen)
83{
84 if (s_ftp.rx_consumed > 0)
85 {
86 memmove(s_ftp.rx, s_ftp.rx + s_ftp.rx_consumed, s_ftp.rx_len - s_ftp.rx_consumed);
87 s_ftp.rx_len -= s_ftp.rx_consumed;
88 s_ftp.rx_consumed = 0;
89 }
90
91 uint32_t deadline = pc_millis() + PC_FTP_TIMEOUT_MS;
92 for (;;)
93 {
94 size_t consumed = 0;
95 if (pc_ftp_parse_reply(s_ftp.rx, s_ftp.rx_len, code, &consumed))
96 {
97 s_ftp.rx_consumed = consumed;
98 if (rlen)
99 {
100 *rlen = consumed;
101 }
102 PC_LOGD(LOG_REPLY, (uint32_t)*code);
103 return true;
104 }
105 if (s_ftp.rx_len == sizeof(s_ftp.rx))
106 {
107 PC_LOGW(LOG_REPLY_TOO_BIG, (uint32_t)sizeof(s_ftp.rx));
108 return false; // a reply that cannot fit is malformed, not incomplete
109 }
110 if (pc_client_is_closed(s_ftp.ctrl) && pc_client_available(s_ftp.ctrl) == 0)
111 {
112 PC_LOGW(LOG_CTRL_CLOSED, (uint32_t)s_ftp.rx_len);
113 return false;
114 }
115 // pc_millis is monotonic, so the subtraction is wrap-safe across a rollover.
116 if ((int32_t)(pc_millis() - deadline) >= 0)
117 {
118 PC_LOGW(LOG_REPLY_TIMEOUT, (uint32_t)s_ftp.rx_len);
119 return false;
120 }
121
122 size_t got = pc_client_read(s_ftp.ctrl, (uint8_t *)s_ftp.rx + s_ftp.rx_len, sizeof(s_ftp.rx) - s_ftp.rx_len);
123 if (got == 0)
124 {
125 pcdelay(5);
126 }
127 else
128 {
129 s_ftp.rx_len += got;
130 }
131 }
132}
133
134/** @brief Send a command and require a 2xx completion. */
135static bool ftp_cmd_ok(const char *verb, const char *arg)
136{
137 int code = 0;
138 return ftp_send(verb, arg) && ftp_await(&code, nullptr) && pc_ftp_reply_ok(code);
139}
140
141// ---------------------------------------------------------------------------
142// Data channel
143// ---------------------------------------------------------------------------
144
145/**
146 * @brief Ask for a passive data port and connect to it.
147 *
148 * EPSV first (RFC 2428): it carries only a port, so it survives the NAT that makes PASV's
149 * advertised address wrong. PASV is the fallback for servers that answer 500 to EPSV.
150 */
151static bool ftp_open_data(const FtpTarget *target)
152{
153 int code = 0;
154 size_t rlen = 0;
155 uint16_t port = 0;
156 char host[48];
157
158 if (ftp_send("EPSV", nullptr) && ftp_await(&code, &rlen) && code == 229 && pc_ftp_parse_epsv(s_ftp.rx, rlen, &port))
159 {
160 // Extended passive mode reuses the control connection's host.
161 strncpy(host, target->host, sizeof(host) - 1);
162 host[sizeof(host) - 1] = '\0';
163 }
164 else
165 {
166 uint8_t ip[4] = {0, 0, 0, 0};
167 if (!ftp_send("PASV", nullptr) || !ftp_await(&code, &rlen) || code != 227 ||
168 !pc_ftp_parse_pasv(s_ftp.rx, rlen, ip, &port))
169 {
170 return false;
171 }
172 pc_sb sb_host = {host, sizeof(host), 0, true};
173 pc_sb_u32(&sb_host, (uint32_t)((unsigned)ip[0]));
174 pc_sb_put(&sb_host, ".");
175 pc_sb_u32(&sb_host, (uint32_t)((unsigned)ip[1]));
176 pc_sb_put(&sb_host, ".");
177 pc_sb_u32(&sb_host, (uint32_t)((unsigned)ip[2]));
178 pc_sb_put(&sb_host, ".");
179 pc_sb_u32(&sb_host, (uint32_t)((unsigned)ip[3]));
180 if (pc_sb_finish(&sb_host) == 0)
181 {
182 host[0] = '\0';
183 }
184 }
185
186 if (port == 0)
187 {
188 return false;
189 }
190 s_ftp.data = pc_client_open(host, port, PC_FTP_TIMEOUT_MS);
191 if (s_ftp.data < 0)
192 {
193 PC_LOGW(LOG_DATA_CONNECT_FAILED, host, (uint32_t)port);
194 }
195 return s_ftp.data >= 0;
196}
197
198/** @brief Drop both connections and reset the accumulator for the next transfer. */
199static void ftp_teardown(void)
200{
201 if (s_ftp.data >= 0)
202 {
203 pc_client_close(s_ftp.data);
204 s_ftp.data = -1;
205 }
206 if (s_ftp.ctrl >= 0)
207 {
208 pc_client_close(s_ftp.ctrl);
209 s_ftp.ctrl = -1;
210 }
211 s_ftp.rx_len = 0;
212 s_ftp.rx_consumed = 0;
213}
214
215// ---------------------------------------------------------------------------
216// STOR
217// ---------------------------------------------------------------------------
218
219bool pc_ftp_store(const FtpTarget *target, const char *remote_path, size_t total, pc_ftp_source src, void *ctx)
220{
221 if (!target || !target->host || !remote_path || remote_path[0] == '\0' || !src)
222 {
223 return false;
224 }
225 if (s_ftp.ctrl >= 0)
226 {
227 return false; // one transfer at a time
228 }
229
230 uint16_t ctrl_port = target->port ? target->port : 21;
231 s_ftp.rx_len = 0;
232 s_ftp.rx_consumed = 0;
233 s_ftp.ctrl = pc_client_open(target->host, ctrl_port, PC_FTP_TIMEOUT_MS);
234 if (s_ftp.ctrl < 0)
235 {
236 PC_LOGW(LOG_CTRL_CONNECT_FAILED, target->host, (uint32_t)ctrl_port);
237 return false;
238 }
239
240 int code = 0;
241 if (!ftp_await(&code, nullptr) || code != 220) // server greeting
242 {
243 ftp_teardown();
244 return false;
245 }
246
247 // USER answers 331 (password wanted) or 230 (already logged in, e.g. anonymous).
248 if (!ftp_send("USER", target->user ? target->user : "anonymous") || !ftp_await(&code, nullptr))
249 {
250 ftp_teardown();
251 return false;
252 }
253 if (code == 331)
254 {
255 if (!ftp_send("PASS", target->pass ? target->pass : "") || !ftp_await(&code, nullptr))
256 {
257 ftp_teardown();
258 return false;
259 }
260 }
261 if (!pc_ftp_reply_ok(code))
262 {
263 ftp_teardown();
264 return false;
265 }
266
267 // Binary: ASCII mode would rewrite CRLF and corrupt a core dump or any other blob.
268 if (!ftp_cmd_ok("TYPE", "I") || !ftp_open_data(target))
269 {
270 ftp_teardown();
271 return false;
272 }
273
274 // The preliminary 1xx must arrive before any payload; a 5xx here means the path was rejected.
275 if (!ftp_send("STOR", remote_path) || !ftp_await(&code, nullptr) || pc_ftp_reply_class(code) != 1)
276 {
277 ftp_teardown();
278 return false;
279 }
280
281 bool ok = true;
282 size_t off = 0;
283 while (off < total)
284 {
285 size_t want = (total - off < sizeof(s_ftp.chunk)) ? total - off : sizeof(s_ftp.chunk);
286 size_t got = src(ctx, off, s_ftp.chunk, want);
287 if (got != want || !pc_client_send(s_ftp.data, s_ftp.chunk, got))
288 {
289 ok = false;
290 break;
291 }
292 off += got;
293 }
294
295 // Closing the data connection is what marks end-of-file for a STOR, so it happens before the
296 // completion reply is read - and it happens even on failure, so the server stops waiting.
297 pc_client_close(s_ftp.data);
298 s_ftp.data = -1;
299
300 if (ok)
301 {
302 ok = ftp_await(&code, nullptr) && code == 226;
303 }
304
305 ftp_send("QUIT", nullptr); // best effort; the transfer is already decided
306 ftp_teardown();
307 return ok;
308}
309
310#endif // PC_ENABLE_FTP_SESSION
bool pc_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:366
size_t pc_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:374
bool pc_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:362
int pc_client_open(const char *, uint16_t, uint32_t)
Resolve host (dotted-quad fast path, else DNS) and connect to host : port, blocking up to timeout_ms.
Definition client.cpp:354
size_t pc_client_available(int)
Wire bytes currently buffered and ready to read.
Definition client.cpp:370
void pc_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:378
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Pluggable monotonic clock for all library timing.
void pcdelay(uint32_t ms)
Block for at least ms milliseconds - the library's single delay primitive.
Definition clock.h:89
uint32_t pc_millis(void)
The library's monotonic time at 1000 Hz (milliseconds).
Definition clock.h:71
#define PC_END
Definition frame.h:110
#define PC_U32
Definition frame.h:104
@ PC_FK_LIT
literal text from lit; takes no argument
Definition frame.h:56
#define PC_STR
Definition frame.h:103
FTP client wire codec (RFC 959 + RFC 2428 + RFC 3659), PC_ENABLE_FTP.
FTP client session driver: the two sockets the ftp.h codec deliberately does not own.
Abstract logging whose disabled levels cost nothing at all (PC_LOG_LEVEL).
#define PC_LOGW(...)
Definition log.h:89
#define PC_LOGD(...)
Definition log.h:77
#define PC_FTP_CMD_MAX
Suggested FTP control-command buffer size (PC_ENABLE_FTP).
#define PC_FTP_REPLY_BUF
Control-reply accumulator for the FTP session driver (PC_ENABLE_FTP_SESSION).
#define PC_FTP_TIMEOUT_MS
Per-step timeout for the FTP session driver: connect, and each control reply.
#define PC_FTP_CHUNK
Bytes staged per data-channel write when the session driver streams a payload.
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
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
Definition strbuf.h:303
One field of a frame. Frames are static const pc_field[], so they live in rodata.
Definition frame.h:80
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30