mirror of
https://github.com/xoseperez/espurna.git
synced 2026-03-03 15:04:17 +01:00
@@ -143,6 +143,10 @@
|
||||
#define TELNET_MAX_CLIENTS 1 // Max number of concurrent telnet clients
|
||||
#endif
|
||||
|
||||
#ifndef TELNET_LINE_BUFFER_SIZE
|
||||
#define TELNET_LINE_BUFFER_SIZE 256 // Temporary buffer, when data arrives in multiple packets without a new-line
|
||||
#endif
|
||||
|
||||
// Enable this flag to add support for reverse telnet (+800 bytes)
|
||||
// This is useful to telnet to a device behind a NAT or firewall
|
||||
// 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
|
||||
|
||||
@@ -54,6 +54,8 @@ constexpr bool isEspurnaMinimal() {
|
||||
|
||||
namespace build {
|
||||
|
||||
constexpr size_t LineBufferSize { TELNET_LINE_BUFFER_SIZE };
|
||||
|
||||
constexpr size_t ClientsMax { TELNET_MAX_CLIENTS };
|
||||
static_assert(ClientsMax > 0, "");
|
||||
|
||||
@@ -269,6 +271,7 @@ namespace message {
|
||||
|
||||
PROGMEM_STRING(PasswordRequest, "Password (disconnects after 1 failed attempt): ");
|
||||
PROGMEM_STRING(InvalidPassword, "-ERROR: Invalid password\n");
|
||||
PROGMEM_STRING(BufferOverflow, "-ERROR: Buffer overflow\n");
|
||||
PROGMEM_STRING(OkPassword, "+OK\n");
|
||||
|
||||
} // namespace message
|
||||
@@ -312,6 +315,11 @@ struct Address {
|
||||
uint16_t port;
|
||||
};
|
||||
|
||||
::terminal::LineView line_view(pbuf* pb) {
|
||||
auto* payload = reinterpret_cast<const char*>(pb->payload);
|
||||
return StringView{payload, payload + pb->len};
|
||||
}
|
||||
|
||||
Address address(tcp_pcb* pcb) {
|
||||
Address out;
|
||||
ip_addr_copy(out.ip, pcb->remote_ip);
|
||||
@@ -530,46 +538,100 @@ private:
|
||||
}
|
||||
#endif
|
||||
|
||||
struct ProcessLineResult {
|
||||
StringView message;
|
||||
bool close { false };
|
||||
};
|
||||
|
||||
auto process_line(StringView line) -> ProcessLineResult {
|
||||
ProcessLineResult out;
|
||||
if (!line.length()) {
|
||||
return out;
|
||||
}
|
||||
|
||||
switch (_state) {
|
||||
case State::Idle:
|
||||
case State::Connecting:
|
||||
break;
|
||||
case State::Active:
|
||||
#if TERMINAL_SUPPORT
|
||||
process(line.toString());
|
||||
#endif
|
||||
break;
|
||||
case State::Authenticating:
|
||||
if (!systemPasswordEquals(stripNewline(line))) {
|
||||
out.message = StringView{message::InvalidPassword};
|
||||
out.close = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
out.message = StringView{message::OkPassword};
|
||||
_state = State::Active;
|
||||
break;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
err_t on_tcp_recv(pbuf* pb, err_t err) {
|
||||
if (!pb || (err != ERR_OK)) {
|
||||
return close();
|
||||
}
|
||||
|
||||
const auto* payload = reinterpret_cast<const char*>(pb->payload);
|
||||
espurna::terminal::LineView lines({payload, payload + pb->len});
|
||||
|
||||
while (lines) {
|
||||
const auto line = lines.line();
|
||||
if (!line.length()) {
|
||||
break;
|
||||
// We always attempt to parse the network buffer directly first,
|
||||
// socat, netcat, etc. usually send a single packet per line.
|
||||
// Otherwise, try to buffer it and everything else in the chain.
|
||||
for (auto it = pb; it != nullptr; it = it->next) {
|
||||
auto view = line_view(it);
|
||||
if (_line_buffer.size()) {
|
||||
goto next;
|
||||
}
|
||||
|
||||
switch (_state) {
|
||||
case State::Idle:
|
||||
case State::Connecting:
|
||||
break;
|
||||
case State::Active:
|
||||
#if TERMINAL_SUPPORT
|
||||
process(String(line));
|
||||
#endif
|
||||
break;
|
||||
case State::Authenticating:
|
||||
if (!systemPasswordEquals(stripNewline(line))) {
|
||||
write_message(message::InvalidPassword);
|
||||
return close();
|
||||
for (auto line = view.line(); line.length() > 0; line = view.line()) {
|
||||
auto result = process_line(line);
|
||||
if (result.message.length()) {
|
||||
write_message(result.message);
|
||||
}
|
||||
|
||||
write_message(message::OkPassword);
|
||||
if (result.close) {
|
||||
return close();
|
||||
}
|
||||
}
|
||||
|
||||
_state = State::Active;
|
||||
break;
|
||||
next:
|
||||
if (view.length()) {
|
||||
_line_buffer.append(view.get());
|
||||
}
|
||||
}
|
||||
|
||||
// Right now, only accept simple payloads that are limited by TCP_MSS
|
||||
// In case there are more than one `pbuf` chained together, we discrard
|
||||
// everything else and only use the first available one
|
||||
// (and, only if it contains line breaks; everything else is lost)
|
||||
if (_line_buffer.overflow()) {
|
||||
write_message(message::BufferOverflow);
|
||||
return close();
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
const auto line_result = _line_buffer.line();
|
||||
if (line_result.overflow) {
|
||||
write_message(message::BufferOverflow);
|
||||
return close();
|
||||
}
|
||||
|
||||
if (!line_result.line.length()) {
|
||||
break;
|
||||
}
|
||||
|
||||
auto result = process_line(line_result.line);
|
||||
if (result.message.length()) {
|
||||
write_message(result.message);
|
||||
}
|
||||
|
||||
if (result.close) {
|
||||
return close();
|
||||
}
|
||||
}
|
||||
|
||||
// expect everything to be handled above, we don't allow lingering pbufs
|
||||
// (as extra buffers, for retries, or anything else)
|
||||
tcp_recved(_pcb, pb->tot_len);
|
||||
pbuf_free(pb);
|
||||
|
||||
@@ -608,6 +670,7 @@ private:
|
||||
bool _request_auth { false };
|
||||
|
||||
#if TERMINAL_SUPPORT
|
||||
::terminal::LineBuffer<build::LineBufferSize> _line_buffer;
|
||||
std::list<String> _cmds;
|
||||
#endif
|
||||
ClientWriter _writer;
|
||||
|
||||
@@ -147,20 +147,20 @@ struct LineView {
|
||||
{}
|
||||
|
||||
StringView line() {
|
||||
const auto begin = _lines.begin() + _cursor;
|
||||
const auto end = _lines.end();
|
||||
const auto Begin = begin();
|
||||
const auto End = begin();
|
||||
|
||||
if (begin != end) {
|
||||
const auto eol = std::find(begin, end, '\n');
|
||||
if (eol != end) {
|
||||
if (Begin != End) {
|
||||
const auto eol = std::find(begin(), end(), '\n');
|
||||
if (eol != End) {
|
||||
const auto after = std::next(eol);
|
||||
if (after != end) {
|
||||
if (after != End) {
|
||||
_cursor = std::distance(_lines.begin(), after);
|
||||
} else {
|
||||
_cursor = _lines.length();
|
||||
}
|
||||
|
||||
return StringView{begin, after};
|
||||
return StringView{Begin, after};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +171,22 @@ struct LineView {
|
||||
return _cursor != _lines.length();
|
||||
}
|
||||
|
||||
const char* begin() const {
|
||||
return _lines.begin() + _cursor;
|
||||
}
|
||||
|
||||
const char* end() const {
|
||||
return _lines.end();
|
||||
}
|
||||
|
||||
size_t length() const {
|
||||
return std::distance(begin(), end());
|
||||
}
|
||||
|
||||
StringView get() const {
|
||||
return StringView{begin(), end()};
|
||||
}
|
||||
|
||||
private:
|
||||
StringView _lines;
|
||||
uintptr_t _cursor { 0 };
|
||||
|
||||
Reference in New Issue
Block a user