Add Telnet server using command Telnet <0|1|port>[,<IP filter>]

This commit is contained in:
Theo Arends 2025-03-20 23:11:30 +01:00
parent fb6640b91f
commit fbb752d8c2
12 changed files with 264 additions and 18 deletions

View File

@ -19,6 +19,7 @@ Note: the `minimal` variant is not listed as it shouldn't be used outside of the
| USE_4K_RSA | - | - / - | - | - | - | - |
| USE_TELEGRAM | - | - / - | - | - | - | - |
| USE_KNX | - | - / x | x | - | - | - |
| USE_TELNET | - | - / - | - | - | - | - |
| USE_WEBSERVER | x | x / x | x | x | x | x |
| USE_WEBSEND_RESPONSE | - | - / - | - | - | - | - |
| USE_EMULATION_HUE | x | x / x | - | x | - | - |

View File

@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
- Support for HLK-LD2402 24GHz smart wave motion sensor (#23133)
- Matter prepare for ICD cluster (#23158)
- Berry `re.dump()` (#23162)
- Telnet server using command `Telnet <0|1|port>[,<IP filter>]`
### Breaking Changed
- Berry remove `Leds.create_matrix` from the standard library waiting for reimplementation (#23114)

View File

@ -89,8 +89,9 @@ In addition to @arendst the following code is mainly owned by:
| xdrv_75_dali | @eeak, @arendst
| xdrv_76_serial_i2c | @s-hadinger
| xdrv_77_wizmote | @arendst
| xdrv_78 |
| xdrv_78_telnet | @arendst
| xdrv_79_esp32_ble | @staars, @btsimonh
| xdrv_80 |
| xdrv_81_esp32_webcam | @gemu, @philrich
| xdrv_82_esp32_ethernet | @arendst
| xdrv_83_esp32_watch | @gemu

View File

@ -116,6 +116,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm
## Changelog v14.5.0.2
### Added
- Telnet server using command `Telnet <0|1|port>[,<IP filter>]`
- Support Vango Technologies V924x ultralow power, single-phase, power measurement [#23127](https://github.com/arendst/Tasmota/issues/23127)
- Support for HLK-LD2402 24GHz smart wave motion sensor [#23133](https://github.com/arendst/Tasmota/issues/23133)
- Allow acl in mqtt when client certificate is in use with `#define USE_MQTT_CLIENT_CERT` [#22998](https://github.com/arendst/Tasmota/issues/22998)

View File

@ -47,8 +47,11 @@ extern "C" int startWaveformClockCycles(uint8_t pin, uint32_t highCcys, uint32_t
uint32_t runTimeCcys, int8_t alignPhase, uint32_t phaseOffsetCcys, bool autoPwm);
extern "C" void setTimer1Callback(uint32_t (*fn)());
#ifdef USE_SERIAL_BRIDGE
void SerialBridgePrintf(PGM_P formatP, ...);
void SerialBridgePrint(char *data);
#endif
#ifdef USE_TELNET
void TelnetPrint(char *data);
#endif // USE_TELNET
#ifdef USE_INFLUXDB
void InfluxDbProcess(bool use_copy = false);
#endif

View File

@ -485,6 +485,9 @@
//#define USE_KNX // Enable KNX IP Protocol Support (+9.4k code, +3k7 mem)
#define USE_KNX_WEB_MENU // Enable KNX WEB MENU (+8.3k code, +144 mem)
// -- Telnet --------------------------------------
//#define USE_TELNET // Add support for telnet (+1k3 code)
// -- HTTP ----------------------------------------
#define USE_WEBSERVER // Enable web server and Wi-Fi Manager (+66k code, +8k mem)
#define WEB_PORT 80 // Web server Port for User and Admin mode

View File

@ -2639,16 +2639,26 @@ void AddLogData(uint32_t loglevel, const char* log_data, const char* log_data_pa
if ((loglevel <= TasmotaGlobal.seriallog_level) &&
(TasmotaGlobal.masterlog_level <= TasmotaGlobal.seriallog_level)) {
TasConsole.printf("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained);
char* data = ext_snprintf_malloc_P("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained);
if (data) {
TasConsole.print(data);
#ifdef USE_SERIAL_BRIDGE
SerialBridgePrintf("%s%s%s%s\r\n", mxtime, log_data, log_data_payload, log_data_retained);
SerialBridgePrint(data);
#endif // USE_SERIAL_BRIDGE
#ifdef USE_TELNET
TelnetPrint(data);
#endif // USE_TELNET
free(data);
}
}
if (!TasmotaGlobal.log_buffer) { return; } // Leave now if there is no buffer available
uint32_t highest_loglevel = Settings->weblog_level;
uint32_t highest_loglevel = Settings->seriallog_level; // Need this for Telnet
if (Settings->mqttlog_level > highest_loglevel) { highest_loglevel = Settings->mqttlog_level; }
#ifdef USE_WEBSERVER
if (Settings->weblog_level > highest_loglevel) { highest_loglevel = Settings->weblog_level; }
#endif // USE_WEBSERVER
#ifdef USE_UFILESYS
uint32_t filelog_level = Settings->filelog_level % 10;
if (filelog_level > highest_loglevel) { highest_loglevel = filelog_level; }

View File

@ -946,7 +946,9 @@ constexpr uint32_t feature[] = {
#if defined(USE_ENERGY_SENSOR) && defined(USE_V9240)
0x00004000 | // xnrg_25_v9240.ino
#endif
// 0x00008000 | //
#ifdef USE_TELNET
0x00008000 | // xdrv_80_telnet.ino
#endif
// 0x00010000 | //
// 0x00020000 | //
// 0x00040000 | //

View File

@ -99,18 +99,11 @@ void SetSSerialConfig(uint32_t serial_config) {
}
}
void SerialBridgePrintf(PGM_P formatP, ...) {
void SerialBridgePrint(char *data) {
#ifdef USE_SERIAL_BRIDGE_TEE
if ((SB_TEE == Settings->sserial_mode) && serial_bridge_buffer) {
va_list arg;
va_start(arg, formatP);
char* data = ext_vsnprintf_malloc_P(formatP, arg);
va_end(arg);
if (data == nullptr) { return; }
// SerialBridgeSerial->printf(data); // This resolves "MqttClientMask":"DVES_%06X" into "DVES_000002"
SerialBridgeSerial->print(data); // This does not resolve "DVES_%06X"
free(data);
}
#endif // USE_SERIAL_BRIDGE_TEE
}
@ -274,7 +267,8 @@ void SerialBridgeInit(void) {
AddLog(LOG_LEVEL_DEBUG, PSTR("SBR: Serial UART%d"), SerialBridgeSerial->getUart());
#endif
SerialBridgeSerial->flush();
SerialBridgePrintf("\r\n");
char data[] = "\r\n";
SerialBridgePrint(data);
}
}
}

View File

@ -1089,8 +1089,11 @@ extern "C" {
if (len+3 > LOGSZ) { strcat(log_data, "..."); } // Actual data is more
TasConsole.printf(log_data);
#ifdef USE_SERIAL_BRIDGE
SerialBridgePrintf(log_data);
SerialBridgePrint(log_data);
#endif // USE_SERIAL_BRIDGE
#ifdef USE_TELNET
TelnetPrint(log_data);
#endif // USE_TELNET
}
void berry_log_C(const char * berry_buf, ...) {

View File

@ -0,0 +1,227 @@
/*
xdrv_78_telnet.ino - Telnet console support for Tasmota
SPDX-FileCopyrightText: 2025 Theo Arends
SPDX-License-Identifier: GPL-3.0-only
*/
#ifdef USE_TELNET
/*********************************************************************************************\
* Telnet console support for a single connection
\*********************************************************************************************/
#define XDRV_78 78
#ifndef TELNET_BUF_SIZE
#define TELNET_BUF_SIZE 255 // size of the buffer
#endif
struct {
WiFiServer *server = nullptr;
WiFiClient client;
IPAddress ip_filter;
char *buffer = nullptr; // data transfer buffer
uint16_t port;
uint16_t buffer_size = TELNET_BUF_SIZE;
bool ip_filter_enabled = false;
} Telnet;
/********************************************************************************************/
void TelnetPrint(char *data) {
if (Telnet.server) {
WiFiClient &client = Telnet.client;
if (client) {
// client.printf(data); // This resolves "MqttClientMask":"DVES_%06X" into "DVES_000002"
client.print(data); // This does not resolve "DVES_%06X"
}
}
}
/********************************************************************************************/
void TelnetLoop(void) {
// check for a new client connection
if ((Telnet.server) && (Telnet.server->hasClient())) {
WiFiClient new_client = Telnet.server->available();
AddLog(LOG_LEVEL_INFO, PSTR("TLN: Connection from %s"), new_client.remoteIP().toString().c_str());
if (Telnet.ip_filter_enabled) { // Check for IP filtering if it's enabled
if (Telnet.ip_filter != new_client.remoteIP()) {
AddLog(LOG_LEVEL_INFO, PSTR("TLN: Rejected due to filtering"));
new_client.stop();
} else {
AddLog(LOG_LEVEL_INFO, PSTR("TLN: Allowed through filter"));
}
}
WiFiClient &client = Telnet.client;
if (client) {
client.stop();
}
client = new_client;
if (client) {
client.printf("Tasmota %s %s (%s)\r\n\n", TasmotaGlobal.hostname, TasmotaGlobal.version, GetBuildDateAndTime().c_str());
uint32_t index = 1;
char* line;
size_t len;
while (GetLog(TasmotaGlobal.seriallog_level, &index, &line, &len)) {
// [14:49:36.123 MQTT: stat/wemos5/RESULT = {"POWER":"OFF"}] > [{"POWER":"OFF"}]
client.write(line, len -1);
client.write("\r\n");
}
client.printf("%s:# ", TasmotaGlobal.hostname);
}
}
bool busy;
uint32_t buf_len = 0;
do {
busy = false; // exit loop if no data was transferred
WiFiClient &client = Telnet.client;
bool overrun = false;
while (client && (client.available())) {
uint8_t c = client.read();
if (c >= 0) {
busy = true;
if (isprint(c)) { // Any char between 32 and 127
if (buf_len < Telnet.buffer_size -1) { // Add char to string if it still fits
Telnet.buffer[buf_len++] = c;
} else {
overrun = true; // Signal overrun but continue reading input to flush until '\n' (EOL)
}
}
else if (c == '\n') {
Telnet.buffer[buf_len] = 0; // Telnet data completed
TasmotaGlobal.seriallog_level = (Settings->seriallog_level < LOG_LEVEL_INFO) ? (uint8_t)LOG_LEVEL_INFO : Settings->seriallog_level;
if (overrun) {
AddLog(LOG_LEVEL_INFO, PSTR("TLN: buffer overrun"));
} else {
AddLog(LOG_LEVEL_INFO, PSTR("TLN: %s"), Telnet.buffer);
ExecuteCommand(Telnet.buffer, SRC_REMOTE);
}
client.flush();
client.printf("%s:# ", TasmotaGlobal.hostname);
return;
}
}
}
yield(); // avoid WDT if heavy traffic
} while (busy);
}
/*********************************************************************************************\
* Commands
\*********************************************************************************************/
void TelnetStop(void) {
Telnet.server->stop();
delete Telnet.server;
Telnet.server = nullptr;
WiFiClient &client = Telnet.client;
client.stop();
free(Telnet.buffer);
Telnet.buffer = nullptr;
}
const char kTelnetCommands[] PROGMEM = "Telnet|" // prefix
"|Buffer";
void (* const TelnetCommand[])(void) PROGMEM = {
&CmndTelnet, &CmndTelnetBuffer };
void CmndTelnet(void) {
// Telnet - Show telnet server state
// Telnet 0 - Disable telnet server
// Telnet 1 - Enable telnet server on port 23
// Telnet 23 - Enable telnet server on port 23
// Telnet 1, 192.168.2.1 - Enable telnet server and only allow connection from 192.168.2.1
if (!TasmotaGlobal.global_state.network_down) {
if (XdrvMailbox.data_len) {
Telnet.port = XdrvMailbox.payload;
if (ArgC() == 2) {
char sub_string[XdrvMailbox.data_len];
Telnet.ip_filter.fromString(ArgV(sub_string, 2));
Telnet.ip_filter_enabled = true;
} else {
// Disable whitelist if previously set
Telnet.ip_filter_enabled = false;
}
if (Telnet.server) {
TelnetStop();
}
if (Telnet.port > 0) {
if (!Telnet.buffer) {
Telnet.buffer = (char*)malloc(Telnet.buffer_size);
if (!Telnet.buffer) { return; }
if (1 == Telnet.port) { Telnet.port = 23; }
Telnet.server = new WiFiServer(Telnet.port);
Telnet.server->begin(); // start TCP server
Telnet.server->setNoDelay(true);
}
}
}
if (Telnet.server) {
ResponseCmndChar_P(PSTR("Started"));
} else {
ResponseCmndChar_P(PSTR("Stopped"));
}
}
}
void CmndTelnetBuffer(void) {
// TelnetBuffer - Show current input buffer size (default 255)
// TelnetBuffer 300 - Change input buffer size to 300 characters
if (XdrvMailbox.data_len > 0) {
uint16_t bsize = Telnet.buffer_size;
Telnet.buffer_size = XdrvMailbox.payload;
if (XdrvMailbox.payload < MIN_INPUT_BUFFER_SIZE) {
Telnet.buffer_size = MIN_INPUT_BUFFER_SIZE; // 256 / 256
}
else if (XdrvMailbox.payload > INPUT_BUFFER_SIZE) {
Telnet.buffer_size = INPUT_BUFFER_SIZE; // 256 / 800
}
if (Telnet.buffer && (bsize != Telnet.buffer_size)) {
Telnet.buffer = (char*)realloc(Telnet.buffer, Telnet.buffer_size);
if (!Telnet.buffer) {
TelnetStop();
ResponseCmndChar_P(PSTR("Stopped"));
return;
}
}
}
ResponseCmndNumber(Telnet.buffer_size);
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
bool Xdrv78(uint32_t function) {
bool result = false;
if (FUNC_COMMAND == function) {
result = DecodeCommand(kTelnetCommands, TelnetCommand);
} else if (Telnet.buffer) {
switch (function) {
case FUNC_LOOP:
TelnetLoop();
break;
case FUNC_ACTIVE:
result = true;
break;
}
}
return result;
}
#endif // USE_TELNET

View File

@ -311,7 +311,7 @@ a_features = [[
"USE_MAGIC_SWITCH","USE_PIPSOLAR","USE_GPIO_VIEWER","USE_AMSX915",
"USE_SPI_LORA","USE_SPL06_007","USE_QMP6988","USE_WOOLIIS",
"USE_HX711_M5SCALES","USE_RX8010","USE_PCF85063","USE_ESP32_TWAI",
"USE_C8_CO2_5K","USE_WIZMOTE","USE_V9240","",
"USE_C8_CO2_5K","USE_WIZMOTE","USE_V9240","USE_TELNET",
"","","","",
"","","","",
"","","","",