ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
telnet.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 telnet.cpp
6 * @brief Minimal RFC 854 Telnet server implementation.
7 */
8
10
11#if PC_ENABLE_TELNET
12
15#include <stdarg.h>
16#include <string.h>
17
18// Telnet protocol bytes (RFC 854 / 858 / 857): wire values compared/emitted, so integer constants
19// in a namespacing struct.
20struct TelnetByte
21{
22 static constexpr uint8_t T_SE = 240;
23 static constexpr uint8_t T_SB = 250;
24 static constexpr uint8_t T_WILL = 251;
25 static constexpr uint8_t T_WONT = 252;
26 static constexpr uint8_t T_DO = 253;
27 static constexpr uint8_t T_DONT = 254;
28 static constexpr uint8_t T_IAC = 255;
29 static constexpr uint8_t OPT_ECHO = 1;
30 static constexpr uint8_t OPT_SGA = 3;
31};
32
33// IAC parser state per connection (a mutually-exclusive state, not a wire value).
34enum class TelnetState : uint8_t
35{
36 TN_NORMAL,
37 TN_IAC,
38 TN_OPT,
39 TN_SB
40};
41
42struct TelnetConn
43{
44 uint8_t slot;
45 bool used;
46 TelnetState st; // IAC parser state
47 uint8_t cmd; // pending WILL/WONT/DO/DONT
48 uint16_t len; // bytes in line[]
49 char line[TELNET_BUF_SIZE];
50};
51
52// All telnet presentation state, owned by one instance (internal linkage): the per-slot
53// connection table and the command callback. One named owner, unreachable cross-TU.
54struct TelnetCtx
55{
56 TelnetConn tn[MAX_TELNET_CONNS];
57 TelnetCommandCb cmd_cb = nullptr;
58};
59static TelnetCtx s_telnet;
60
61static TelnetConn *find_conn(uint8_t slot)
62{
63 for (int i = 0; i < MAX_TELNET_CONNS; i++)
64 {
65 if (s_telnet.tn[i].used && s_telnet.tn[i].slot == slot)
66 {
67 return &s_telnet.tn[i];
68 }
69 }
70 return nullptr;
71}
72
73static void raw_send(uint8_t slot, const void *data, size_t n)
74{
75 if (!pc_conn_active(slot) || // GCOVR_EXCL_BR_LINE n==0 is unreachable: every call site below passes a
76 // fixed nonzero literal length. (Marker must sit on this line: gcov attributes
77 // the whole multi-line condition's branches to the "if" line, not the operand's
78 // own line - a marker on the next line silently fails to exclude anything.)
79 n == 0)
80 {
81 return;
82 }
83 pc_conn_send(slot, data, (u16_t)n);
84 pc_conn_flush(slot);
85}
86
87// Send Telnet *data* (echo + application output): a literal IAC byte (0xFF) MUST be
88// doubled so the client does not read it as a command introducer (RFC 854). Sends
89// runs of non-IAC bytes directly and emits "\xff\xff" for each IAC. Protocol commands
90// (IAC WILL/DO/...) use raw_send directly - they send IAC intentionally.
91static void send_escaped(uint8_t slot, const void *data, size_t n)
92{
93 if (!pc_conn_active(slot) || n == 0)
94 {
95 return;
96 }
97 const uint8_t *b = (const uint8_t *)data;
98 size_t start = 0;
99 for (size_t i = 0; i < n; i++)
100 {
101 if (b[i] == 0xFF)
102 {
103 if (i > start)
104 {
105 pc_conn_send(slot, b + start, (u16_t)(i - start));
106 }
107 pc_conn_send(slot, "\xff\xff", 2); // doubled IAC
108 start = i + 1;
109 }
110 }
111 if (n > start)
112 {
113 pc_conn_send(slot, b + start, (u16_t)(n - start));
114 }
115 pc_conn_flush(slot);
116}
117
118// ---------------------------------------------------------------------------
119// Connection lifecycle (called from the session layer)
120// ---------------------------------------------------------------------------
121
122void pc_telnet_accept(uint8_t slot)
123{
124 TelnetConn *t = nullptr;
125 for (int i = 0; i < MAX_TELNET_CONNS; i++)
126 {
127 if (!s_telnet.tn[i].used)
128 {
129 t = &s_telnet.tn[i];
130 break;
131 }
132 }
133 if (!t)
134 {
135 // No Telnet capacity: drop the connection (transport owns the teardown).
136 pc_conn_close(slot);
137 return;
138 }
139 memset(t, 0, sizeof(*t));
140 t->used = true;
141 t->slot = slot;
142 t->st = TelnetState::TN_NORMAL;
143
144 // Server-side echo + character-at-a-time (suppress go-ahead).
145 static const uint8_t neg[] = {TelnetByte::T_IAC, TelnetByte::T_WILL, TelnetByte::OPT_ECHO,
146 TelnetByte::T_IAC, TelnetByte::T_WILL, TelnetByte::OPT_SGA};
147 raw_send(slot, neg, sizeof(neg));
148 raw_send(slot, "PC Telnet ready\r\n> ", 22);
149}
150
151void pc_telnet_close(uint8_t slot)
152{
153 TelnetConn *t = find_conn(slot);
154 if (t)
155 {
156 t->used = false;
157 }
158}
159
160// Process one decoded data byte (not part of an IAC sequence).
161static void handle_data(uint8_t slot, TelnetConn *t, uint8_t b)
162{
163 if (b == '\r')
164 {
165 return; // wait for the LF of CRLF
166 }
167 if (b == '\n')
168 {
169 t->line[t->len] = '\0';
170 raw_send(slot, "\r\n", 2);
171 if (s_telnet.cmd_cb)
172 {
173 s_telnet.cmd_cb(t->line, (uint8_t)(t - s_telnet.tn));
174 }
175 t->len = 0;
176 raw_send(slot, "> ", 2);
177 return;
178 }
179 if (b == 0x08 || b == 0x7F) // backspace / delete
180 {
181 if (t->len > 0)
182 {
183 t->len--;
184 raw_send(slot, "\b \b", 3);
185 }
186 return;
187 }
188 if (b < 0x20) // ignore other control characters
189 {
190 return;
191 }
192 if (t->len < sizeof(t->line) - 1)
193 {
194 t->line[t->len++] = (char)b;
195 send_escaped(slot, &b, 1); // echo (doubles a literal IAC per RFC 854)
196 }
197}
198
199void pc_telnet_rx(uint8_t slot)
200{
201 TelnetConn *t = find_conn(slot);
202 if (!t)
203 {
204 return;
205 }
206
207 uint8_t b;
208 while (pc_conn_read_byte(slot, &b))
209 {
210 switch (t->st) // GCOVR_EXCL_BR_LINE t->st is a TelnetState enum class and every enumerator
211 // (TN_NORMAL/TN_IAC/TN_OPT/TN_SB) has a case below; the compiler's defensive "no case matched"
212 // branch can't be reached from any host input
213 {
214 case TelnetState::TN_NORMAL:
215 if (b == TelnetByte::T_IAC)
216 {
217 t->st = TelnetState::TN_IAC;
218 }
219 else
220 {
221 handle_data(slot, t, b);
222 }
223 break;
224 case TelnetState::TN_IAC:
225 if (b == TelnetByte::T_SB)
226 {
227 t->st = TelnetState::TN_SB;
228 }
229 else if (b == TelnetByte::T_WILL || b == TelnetByte::T_WONT || b == TelnetByte::T_DO ||
230 b == TelnetByte::T_DONT)
231 {
232 t->cmd = b;
233 t->st = TelnetState::TN_OPT;
234 }
235 else if (b == TelnetByte::T_IAC)
236 {
237 handle_data(slot, t, 0xFF); // escaped literal 0xFF
238 t->st = TelnetState::TN_NORMAL;
239 }
240 else
241 {
242 t->st = TelnetState::TN_NORMAL; // other 2-byte command (GA, NOP, ...) - consume
243 }
244 break;
245 case TelnetState::TN_OPT: {
246 // Refuse what we don't actively support; stay quiet on options we
247 // already offered (ECHO/SGA) to avoid negotiation loops.
248 uint8_t reply = 0;
249 if (t->cmd == TelnetByte::T_DO && b != TelnetByte::OPT_ECHO && b != TelnetByte::OPT_SGA)
250 {
251 reply = TelnetByte::T_WONT;
252 }
253 else if (t->cmd == TelnetByte::T_WILL)
254 {
255 reply = TelnetByte::T_DONT;
256 }
257 if (reply)
258 {
259 uint8_t resp[3] = {TelnetByte::T_IAC, reply, b};
260 raw_send(slot, resp, 3);
261 }
262 t->st = TelnetState::TN_NORMAL;
263 break;
264 }
265 case TelnetState::TN_SB:
266 if (b == TelnetByte::T_SE)
267 {
268 t->st = TelnetState::TN_NORMAL; // end of subnegotiation (contents ignored)
269 }
270 break;
271 }
272 }
273}
274
275// ---------------------------------------------------------------------------
276// Application API
277// ---------------------------------------------------------------------------
278
279void pc_telnet_on_command(TelnetCommandCb cb)
280{
281 s_telnet.cmd_cb = cb;
282}
283
284static void broadcast(const char *s, size_t n)
285{
286 for (int i = 0; i < MAX_TELNET_CONNS; i++)
287 {
288 if (s_telnet.tn[i].used)
289 {
290 send_escaped(s_telnet.tn[i].slot, s, n); // app output: escape IAC (RFC 854)
291 }
292 }
293}
294
295void pc_telnet_print(const char *s)
296{
297 if (s)
298 {
299 broadcast(s, strnlen(s, TELNET_BUF_SIZE)); // line-oriented console, same cap as pc_telnet_printf
300 }
301}
302
303void pc_telnet_println(const char *s)
304{
305 if (s)
306 {
307 broadcast(s, strnlen(s, TELNET_BUF_SIZE));
308 }
309 broadcast("\r\n", 2);
310}
311
312void pc_telnet_frame(const pc_field *spec, ...)
313{
314 char buf[TELNET_BUF_SIZE];
315 va_list ap;
316 va_start(ap, spec);
317 size_t n = pc_frame_vbuild(buf, sizeof(buf), spec, ap);
318 va_end(ap);
319 if (n > 0)
320 {
321 broadcast(buf, n);
322 }
323}
324
325uint8_t pc_telnet_client_count()
326{
327 uint8_t c = 0;
328 for (int i = 0; i < MAX_TELNET_CONNS; i++)
329 {
330 if (s_telnet.tn[i].used)
331 {
332 c++;
333 }
334 }
335 return c;
336}
337
338// The Telnet ProtoHandler (Layer 5 dispatch seam) - installed by proto_register_builtins() via this
339// accessor, so this module carries no dependency on the session layer.
340static const ProtoHandler s_telnet_handler = {pc_telnet_accept, pc_telnet_rx, pc_telnet_close, nullptr};
341const ProtoHandler *pc_telnet_proto_handler(void)
342{
343 return &s_telnet_handler;
344}
345
346#endif // PC_ENABLE_TELNET
PC_OPTIMIZE_O2 size_t pc_frame_vbuild(char *out, size_t cap, const pc_field *spec, va_list ap)
va_list form, for a caller that already has one.
Definition frame.cpp:25
Layer 5 (Session) - per-protocol connection handler dispatch table.
#define MAX_TELNET_CONNS
Maximum simultaneous Telnet connections.
#define TELNET_BUF_SIZE
Stack buffer for one Telnet I/O chunk.
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).
One field of a frame. Frames are static const pc_field[], so they live in rodata.
Definition frame.h:80
void pc_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:561
void pc_conn_close(uint8_t slot)
Close connection slot gracefully (tcp_close), aborting if the FIN cannot be queued....
Definition tcp.cpp:645
bool pc_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:500
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
Layer 6/7 - minimal RFC 854 Telnet server (PC_ENABLE_TELNET).