DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 DETWS_ENABLE_TELNET
12
15#include <stdarg.h>
16#include <stdio.h>
17#include <string.h>
18
19// Telnet protocol bytes (RFC 854 / 858 / 857): wire values compared/emitted, so integer constants
20// in a namespacing struct.
21struct TelnetByte
22{
23 static constexpr uint8_t T_SE = 240;
24 static constexpr uint8_t T_SB = 250;
25 static constexpr uint8_t T_WILL = 251;
26 static constexpr uint8_t T_WONT = 252;
27 static constexpr uint8_t T_DO = 253;
28 static constexpr uint8_t T_DONT = 254;
29 static constexpr uint8_t T_IAC = 255;
30 static constexpr uint8_t OPT_ECHO = 1;
31 static constexpr uint8_t OPT_SGA = 3;
32};
33
34// IAC parser state per connection (a mutually-exclusive state, not a wire value).
35enum class TelnetState : uint8_t
36{
37 TN_NORMAL,
38 TN_IAC,
39 TN_OPT,
40 TN_SB
41};
42
43struct TelnetConn
44{
45 uint8_t slot;
46 bool used;
47 TelnetState st; // IAC parser state
48 uint8_t cmd; // pending WILL/WONT/DO/DONT
49 uint16_t len; // bytes in line[]
50 char line[TELNET_BUF_SIZE];
51};
52
53// All telnet presentation state, owned by one instance (internal linkage): the per-slot
54// connection table and the command callback. One named owner, unreachable cross-TU.
55struct TelnetCtx
56{
57 TelnetConn tn[MAX_TELNET_CONNS];
58 TelnetCommandCb cmd_cb = nullptr;
59};
60static TelnetCtx s_telnet;
61
62static TelnetConn *find_conn(uint8_t slot)
63{
64 for (int i = 0; i < MAX_TELNET_CONNS; i++)
65 if (s_telnet.tn[i].used && s_telnet.tn[i].slot == slot)
66 return &s_telnet.tn[i];
67 return nullptr;
68}
69
70static void raw_send(uint8_t slot, const void *data, size_t n)
71{
72 if (!det_conn_active(slot) || n == 0)
73 return;
74 det_conn_send(slot, data, (u16_t)n);
75 det_conn_flush(slot);
76}
77
78// Send Telnet *data* (echo + application output): a literal IAC byte (0xFF) MUST be
79// doubled so the client does not read it as a command introducer (RFC 854). Sends
80// runs of non-IAC bytes directly and emits "\xff\xff" for each IAC. Protocol commands
81// (IAC WILL/DO/...) use raw_send directly - they send IAC intentionally.
82static void send_escaped(uint8_t slot, const void *data, size_t n)
83{
84 if (!det_conn_active(slot) || n == 0)
85 return;
86 const uint8_t *b = (const uint8_t *)data;
87 size_t start = 0;
88 for (size_t i = 0; i < n; i++)
89 {
90 if (b[i] == 0xFF)
91 {
92 if (i > start)
93 det_conn_send(slot, b + start, (u16_t)(i - start));
94 det_conn_send(slot, "\xff\xff", 2); // doubled IAC
95 start = i + 1;
96 }
97 }
98 if (n > start)
99 det_conn_send(slot, b + start, (u16_t)(n - start));
100 det_conn_flush(slot);
101}
102
103// ---------------------------------------------------------------------------
104// Connection lifecycle (called from the session layer)
105// ---------------------------------------------------------------------------
106
107void telnet_accept(uint8_t slot)
108{
109 TelnetConn *t = nullptr;
110 for (int i = 0; i < MAX_TELNET_CONNS; i++)
111 if (!s_telnet.tn[i].used)
112 {
113 t = &s_telnet.tn[i];
114 break;
115 }
116 if (!t)
117 {
118 // No Telnet capacity: drop the connection (transport owns the teardown).
119 det_conn_close(slot);
120 return;
121 }
122 memset(t, 0, sizeof(*t));
123 t->used = true;
124 t->slot = slot;
125 t->st = TelnetState::TN_NORMAL;
126
127 // Server-side echo + character-at-a-time (suppress go-ahead).
128 static const uint8_t neg[] = {TelnetByte::T_IAC, TelnetByte::T_WILL, TelnetByte::OPT_ECHO,
129 TelnetByte::T_IAC, TelnetByte::T_WILL, TelnetByte::OPT_SGA};
130 raw_send(slot, neg, sizeof(neg));
131 raw_send(slot, "DetWS Telnet ready\r\n> ", 22);
132}
133
134void telnet_close(uint8_t slot)
135{
136 TelnetConn *t = find_conn(slot);
137 if (t)
138 t->used = false;
139}
140
141// Process one decoded data byte (not part of an IAC sequence).
142static void handle_data(uint8_t slot, TelnetConn *t, uint8_t b)
143{
144 if (b == '\r')
145 return; // wait for the LF of CRLF
146 if (b == '\n')
147 {
148 t->line[t->len] = '\0';
149 raw_send(slot, "\r\n", 2);
150 if (s_telnet.cmd_cb)
151 s_telnet.cmd_cb(t->line, (uint8_t)(t - s_telnet.tn));
152 t->len = 0;
153 raw_send(slot, "> ", 2);
154 return;
155 }
156 if (b == 0x08 || b == 0x7F) // backspace / delete
157 {
158 if (t->len > 0)
159 {
160 t->len--;
161 raw_send(slot, "\b \b", 3);
162 }
163 return;
164 }
165 if (b < 0x20) // ignore other control characters
166 return;
167 if (t->len < sizeof(t->line) - 1)
168 {
169 t->line[t->len++] = (char)b;
170 send_escaped(slot, &b, 1); // echo (doubles a literal IAC per RFC 854)
171 }
172}
173
174void telnet_rx(uint8_t slot)
175{
176 TelnetConn *t = find_conn(slot);
177 if (!t)
178 return;
179
180 uint8_t b;
181 while (det_conn_read_byte(slot, &b))
182 {
183 switch (t->st)
184 {
185 case TelnetState::TN_NORMAL:
186 if (b == TelnetByte::T_IAC)
187 t->st = TelnetState::TN_IAC;
188 else
189 handle_data(slot, t, b);
190 break;
191 case TelnetState::TN_IAC:
192 if (b == TelnetByte::T_SB)
193 t->st = TelnetState::TN_SB;
194 else if (b == TelnetByte::T_WILL || b == TelnetByte::T_WONT || b == TelnetByte::T_DO ||
195 b == TelnetByte::T_DONT)
196 {
197 t->cmd = b;
198 t->st = TelnetState::TN_OPT;
199 }
200 else if (b == TelnetByte::T_IAC)
201 {
202 handle_data(slot, t, 0xFF); // escaped literal 0xFF
203 t->st = TelnetState::TN_NORMAL;
204 }
205 else
206 t->st = TelnetState::TN_NORMAL; // other 2-byte command (GA, NOP, ...) - consume
207 break;
208 case TelnetState::TN_OPT: {
209 // Refuse what we don't actively support; stay quiet on options we
210 // already offered (ECHO/SGA) to avoid negotiation loops.
211 uint8_t reply = 0;
212 if (t->cmd == TelnetByte::T_DO && b != TelnetByte::OPT_ECHO && b != TelnetByte::OPT_SGA)
213 reply = TelnetByte::T_WONT;
214 else if (t->cmd == TelnetByte::T_WILL)
215 reply = TelnetByte::T_DONT;
216 if (reply)
217 {
218 uint8_t resp[3] = {TelnetByte::T_IAC, reply, b};
219 raw_send(slot, resp, 3);
220 }
221 t->st = TelnetState::TN_NORMAL;
222 break;
223 }
224 case TelnetState::TN_SB:
225 if (b == TelnetByte::T_SE)
226 t->st = TelnetState::TN_NORMAL; // end of subnegotiation (contents ignored)
227 break;
228 }
229 }
230}
231
232// ---------------------------------------------------------------------------
233// Application API
234// ---------------------------------------------------------------------------
235
236void telnet_on_command(TelnetCommandCb cb)
237{
238 s_telnet.cmd_cb = cb;
239}
240
241static void broadcast(const char *s, size_t n)
242{
243 for (int i = 0; i < MAX_TELNET_CONNS; i++)
244 if (s_telnet.tn[i].used)
245 send_escaped(s_telnet.tn[i].slot, s, n); // app output: escape IAC (RFC 854)
246}
247
248void telnet_print(const char *s)
249{
250 if (s)
251 broadcast(s, strnlen(s, TELNET_BUF_SIZE)); // line-oriented console, same cap as telnet_printf
252}
253
254void telnet_println(const char *s)
255{
256 if (s)
257 broadcast(s, strnlen(s, TELNET_BUF_SIZE));
258 broadcast("\r\n", 2);
259}
260
261void telnet_printf(const char *fmt, ...)
262{
263 char buf[TELNET_BUF_SIZE];
264 va_list ap;
265 va_start(ap, fmt);
266 int n = vsnprintf(buf, sizeof(buf), fmt, ap);
267 va_end(ap);
268 if (n > 0)
269 broadcast(buf, (size_t)(n < (int)sizeof(buf) ? n : (int)sizeof(buf) - 1));
270}
271
272uint8_t telnet_client_count()
273{
274 uint8_t c = 0;
275 for (int i = 0; i < MAX_TELNET_CONNS; i++)
276 if (s_telnet.tn[i].used)
277 c++;
278 return c;
279}
280
281// The Telnet ProtoHandler (Layer 5 dispatch seam) - installed by proto_register_builtins() via this
282// accessor, so this module carries no dependency on the session layer.
283static const ProtoHandler s_telnet_handler = {telnet_accept, telnet_rx, telnet_close, nullptr};
284const ProtoHandler *telnet_proto_handler(void)
285{
286 return &s_telnet_handler;
287}
288
289#endif // DETWS_ENABLE_TELNET
#define MAX_TELNET_CONNS
Maximum simultaneous Telnet connections.
#define TELNET_BUF_SIZE
Stack buffer for one Telnet I/O chunk.
Layer 5 (Session) - per-protocol connection handler dispatch table.
Per-protocol connection event/poll callbacks (Layer 5 dispatch vtable).
bool det_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:362
void det_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:413
void det_conn_close(uint8_t slot)
Close connection slot gracefully (tcp_close), aborting if the FIN cannot be queued....
Definition tcp.cpp:467
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
Layer 6/7 - minimal RFC 854 Telnet server (DETWS_ENABLE_TELNET).