Skip to content

Commit 542188d

Browse files
committed
telnet: buffer when eol is missing
resolve #2563
1 parent 988a972 commit 542188d

File tree

3 files changed

+115
-32
lines changed

3 files changed

+115
-32
lines changed

code/espurna/config/general.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,10 @@
143143
#define TELNET_MAX_CLIENTS 1 // Max number of concurrent telnet clients
144144
#endif
145145

146+
#ifndef TELNET_LINE_BUFFER_SIZE
147+
#define TELNET_LINE_BUFFER_SIZE 256 // Temporary buffer, when data arrives in multiple packets without a new-line
148+
#endif
149+
146150
// Enable this flag to add support for reverse telnet (+800 bytes)
147151
// This is useful to telnet to a device behind a NAT or firewall
148152
// To use this feature, start a listen server on a publicly reachable host with e.g. "ncat -vlp <port>" and use the MQTT reverse telnet command to connect

code/espurna/telnet.cpp

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ constexpr bool isEspurnaMinimal() {
5454

5555
namespace build {
5656

57+
constexpr size_t LineBufferSize { TELNET_LINE_BUFFER_SIZE };
58+
5759
constexpr size_t ClientsMax { TELNET_MAX_CLIENTS };
5860
static_assert(ClientsMax > 0, "");
5961

@@ -269,6 +271,7 @@ namespace message {
269271

270272
PROGMEM_STRING(PasswordRequest, "Password (disconnects after 1 failed attempt): ");
271273
PROGMEM_STRING(InvalidPassword, "-ERROR: Invalid password\n");
274+
PROGMEM_STRING(BufferOverflow, "-ERROR: Buffer overflow\n");
272275
PROGMEM_STRING(OkPassword, "+OK\n");
273276

274277
} // namespace message
@@ -312,6 +315,11 @@ struct Address {
312315
uint16_t port;
313316
};
314317

318+
::terminal::LineView line_view(pbuf* pb) {
319+
auto* payload = reinterpret_cast<const char*>(pb->payload);
320+
return StringView{payload, payload + pb->len};
321+
}
322+
315323
Address address(tcp_pcb* pcb) {
316324
Address out;
317325
ip_addr_copy(out.ip, pcb->remote_ip);
@@ -530,46 +538,100 @@ class Client {
530538
}
531539
#endif
532540

541+
struct ProcessLineResult {
542+
StringView message;
543+
bool close { false };
544+
};
545+
546+
auto process_line(StringView line) -> ProcessLineResult {
547+
ProcessLineResult out;
548+
if (!line.length()) {
549+
return out;
550+
}
551+
552+
switch (_state) {
553+
case State::Idle:
554+
case State::Connecting:
555+
break;
556+
case State::Active:
557+
#if TERMINAL_SUPPORT
558+
process(line.toString());
559+
#endif
560+
break;
561+
case State::Authenticating:
562+
if (!systemPasswordEquals(stripNewline(line))) {
563+
out.message = StringView{message::InvalidPassword};
564+
out.close = true;
565+
return out;
566+
}
567+
568+
out.message = StringView{message::OkPassword};
569+
_state = State::Active;
570+
break;
571+
}
572+
573+
return out;
574+
}
575+
533576
err_t on_tcp_recv(pbuf* pb, err_t err) {
534577
if (!pb || (err != ERR_OK)) {
535578
return close();
536579
}
537580

538-
const auto* payload = reinterpret_cast<const char*>(pb->payload);
539-
espurna::terminal::LineView lines({payload, payload + pb->len});
540-
541-
while (lines) {
542-
const auto line = lines.line();
543-
if (!line.length()) {
544-
break;
581+
// We always attempt to parse the network buffer directly first,
582+
// socat, netcat, etc. usually send a single packet per line.
583+
// Otherwise, try to buffer it and everything else in the chain.
584+
for (auto it = pb; it != nullptr; it = it->next) {
585+
auto view = line_view(it);
586+
if (_line_buffer.size()) {
587+
goto next;
545588
}
546589

547-
switch (_state) {
548-
case State::Idle:
549-
case State::Connecting:
550-
break;
551-
case State::Active:
552-
#if TERMINAL_SUPPORT
553-
process(String(line));
554-
#endif
555-
break;
556-
case State::Authenticating:
557-
if (!systemPasswordEquals(stripNewline(line))) {
558-
write_message(message::InvalidPassword);
590+
for (auto line = view.line(); line.length() > 0; line = view.line()) {
591+
auto result = process_line(line);
592+
if (result.message.length()) {
593+
write_message(result.message);
594+
}
595+
596+
if (result.close) {
559597
return close();
560598
}
599+
}
561600

562-
write_message(message::OkPassword);
601+
next:
602+
if (view.length()) {
603+
_line_buffer.append(view.get());
604+
}
605+
}
563606

564-
_state = State::Active;
607+
if (_line_buffer.overflow()) {
608+
write_message(message::BufferOverflow);
609+
return close();
610+
}
611+
612+
for (;;) {
613+
const auto line_result = _line_buffer.line();
614+
if (line_result.overflow) {
615+
write_message(message::BufferOverflow);
616+
return close();
617+
}
618+
619+
if (!line_result.line.length()) {
565620
break;
566621
}
622+
623+
auto result = process_line(line_result.line);
624+
if (result.message.length()) {
625+
write_message(result.message);
626+
}
627+
628+
if (result.close) {
629+
return close();
630+
}
567631
}
568632

569-
// Right now, only accept simple payloads that are limited by TCP_MSS
570-
// In case there are more than one `pbuf` chained together, we discrard
571-
// everything else and only use the first available one
572-
// (and, only if it contains line breaks; everything else is lost)
633+
// expect everything to be handled above, we don't allow lingering pbufs
634+
// (as extra buffers, for retries, or anything else)
573635
tcp_recved(_pcb, pb->tot_len);
574636
pbuf_free(pb);
575637

@@ -608,6 +670,7 @@ class Client {
608670
bool _request_auth { false };
609671

610672
#if TERMINAL_SUPPORT
673+
::terminal::LineBuffer<build::LineBufferSize> _line_buffer;
611674
std::list<String> _cmds;
612675
#endif
613676
ClientWriter _writer;

code/espurna/terminal_parsing.h

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,20 +147,20 @@ struct LineView {
147147
{}
148148

149149
StringView line() {
150-
const auto begin = _lines.begin() + _cursor;
151-
const auto end = _lines.end();
150+
const auto Begin = begin();
151+
const auto End = begin();
152152

153-
if (begin != end) {
154-
const auto eol = std::find(begin, end, '\n');
155-
if (eol != end) {
153+
if (Begin != End) {
154+
const auto eol = std::find(begin(), end(), '\n');
155+
if (eol != End) {
156156
const auto after = std::next(eol);
157-
if (after != end) {
157+
if (after != End) {
158158
_cursor = std::distance(_lines.begin(), after);
159159
} else {
160160
_cursor = _lines.length();
161161
}
162162

163-
return StringView{begin, after};
163+
return StringView{Begin, after};
164164
}
165165
}
166166

@@ -171,6 +171,22 @@ struct LineView {
171171
return _cursor != _lines.length();
172172
}
173173

174+
const char* begin() const {
175+
return _lines.begin() + _cursor;
176+
}
177+
178+
const char* end() const {
179+
return _lines.end();
180+
}
181+
182+
size_t length() const {
183+
return std::distance(begin(), end());
184+
}
185+
186+
StringView get() const {
187+
return StringView{begin(), end()};
188+
}
189+
174190
private:
175191
StringView _lines;
176192
uintptr_t _cursor { 0 };

0 commit comments

Comments
 (0)