- /*
-
- TERMINAL MODULE
-
- Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
- Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
-
- */
-
- #include "espurna.h"
-
- #if TERMINAL_SUPPORT
-
- #include "api.h"
- #include "settings.h"
- #include "system.h"
- #include "telnet.h"
- #include "utils.h"
- #include "mqtt.h"
- #include "wifi.h"
- #include "ws.h"
-
- #include "libs/URL.h"
- #include "libs/StreamAdapter.h"
- #include "libs/PrintString.h"
-
- #include "web_asyncwebprint_impl.h"
-
- #include <algorithm>
- #include <vector>
- #include <utility>
-
- #include <Schedule.h>
- #include <Stream.h>
-
- // not yet CONNECTING or LISTENING
- extern struct tcp_pcb *tcp_bound_pcbs;
- // accepting or sending data
- extern struct tcp_pcb *tcp_active_pcbs;
- // // TIME-WAIT status
- extern struct tcp_pcb *tcp_tw_pcbs;
-
- namespace {
-
- // Based on libs/StreamInjector.h by Xose Pérez <xose dot perez at gmail dot com> (see git-log for more info)
- // Instead of custom write(uint8_t) callback, we provide writer implementation in-place
-
- struct TerminalIO final : public Stream {
-
- TerminalIO(size_t capacity = 128) :
- _buffer(new char[capacity]),
- _capacity(capacity),
- _write(0),
- _read(0)
- {}
-
- ~TerminalIO() {
- delete[] _buffer;
- }
-
- // ---------------------------------------------------------------------
- // Injects data into the internal buffer so we can read() it
- // ---------------------------------------------------------------------
-
- size_t capacity() {
- return _capacity;
- }
-
- size_t inject(char ch) {
- _buffer[_write] = ch;
- _write = (_write + 1) % _capacity;
- return 1;
- }
-
- size_t inject(char *data, size_t len) {
- for (size_t index = 0; index < len; ++index) {
- inject(data[index]);
- }
- return len;
- }
-
- // ---------------------------------------------------------------------
- // XXX: We are only supporting part of the Print & Stream interfaces
- // But, we need to be have all pure virtual methods implemented
- // ---------------------------------------------------------------------
-
- // Return data from the internal buffer
- int available() override {
- unsigned int bytes = 0;
- if (_read > _write) {
- bytes += (_write - _read + _capacity);
- } else if (_read < _write) {
- bytes += (_write - _read);
- }
- return bytes;
- }
-
- int peek() override {
- int ch = -1;
- if (_read != _write) {
- ch = _buffer[_read];
- }
- return ch;
- }
-
- int read() override {
- int ch = -1;
- if (_read != _write) {
- ch = _buffer[_read];
- _read = (_read + 1) % _capacity;
- }
- return ch;
- }
-
- // {Stream,Print}::flush(), see:
- // - https://github.com/esp8266/Arduino/blob/master/cores/esp8266/Print.h
- // - https://github.com/espressif/arduino-esp32/blob/master/cores/esp32/Print.h
- // - https://github.com/arduino/ArduinoCore-API/issues/102
- // Old 2.3.0 expects flush() on Stream, latest puts in in Print
- // We may have to cheat the system and implement everything as Stream to have it available.
- void flush() override {
- // Here, reset reader position so that we return -1 until we have new data
- // writer flushing is implemented below, we don't need it here atm
- _read = _write;
- }
-
- size_t write(const uint8_t* buffer, size_t size) override {
- // Buffer data until we encounter line break, then flush via Raw debug method
- // (which is supposed to 1-to-1 copy the data, without adding the timestamp)
- #if DEBUG_SUPPORT
- if (!size) return 0;
- if (buffer[size-1] == '\0') return 0;
- if (_output.capacity() < (size + 2)) {
- _output.reserve(_output.size() + size + 2);
- }
- _output.insert(_output.end(),
- reinterpret_cast<const char*>(buffer),
- reinterpret_cast<const char*>(buffer) + size
- );
- if (_output.end() != std::find(_output.begin(), _output.end(), '\n')) {
- _output.push_back('\0');
- debugSendRaw(_output.data());
- _output.clear();
- }
- #endif
- return size;
- }
-
- size_t write(uint8_t ch) override {
- uint8_t buffer[1] {ch};
- return write(buffer, 1);
- }
-
- private:
-
- #if DEBUG_SUPPORT
- std::vector<char> _output;
- #endif
-
- char * _buffer;
- unsigned char _capacity;
- unsigned char _write;
- unsigned char _read;
-
- };
-
- auto _io = TerminalIO(TERMINAL_SHARED_BUFFER_SIZE);
- terminal::Terminal _terminal(_io, _io.capacity());
-
- // TODO: re-evaluate how and why this is used
- #if SERIAL_RX_ENABLED
-
- constexpr size_t SerialRxBufferSize { 128u };
- char _serial_rx_buffer[SerialRxBufferSize];
- static unsigned char _serial_rx_pointer = 0;
-
- #endif // SERIAL_RX_ENABLED
-
- // -----------------------------------------------------------------------------
- // Commands
- // -----------------------------------------------------------------------------
-
- void _terminalHelpCommand(const terminal::CommandContext& ctx) {
- auto names = _terminal.names();
-
- // XXX: Core's ..._P funcs only allow 2nd pointer to be in PROGMEM,
- // explicitly load the 1st one
- std::sort(names.begin(), names.end(), [](const __FlashStringHelper* lhs, const __FlashStringHelper* rhs) {
- const String lhs_as_string(lhs);
- return strncasecmp_P(lhs_as_string.c_str(), reinterpret_cast<const char*>(rhs), lhs_as_string.length()) < 0;
- });
-
- ctx.output.print(F("Available commands:\n"));
- for (auto* name : names) {
- ctx.output.printf("> %s\n", reinterpret_cast<const char*>(name));
- }
-
- terminalOK(ctx.output);
- }
-
- namespace dns {
-
- using Callback = std::function<void(const char* name, const ip_addr_t* addr, void* arg)>;
-
- namespace internal {
-
- struct Task {
- Task() = delete;
- explicit Task(String&& hostname, Callback&& callback) :
- _hostname(std::move(hostname)),
- _callback(std::move(callback))
- {}
-
- ip_addr_t* addr() {
- return &_addr;
- }
-
- const char* hostname() const {
- return _hostname.c_str();
- }
-
- void callback(const char* name, const ip_addr_t* addr, void* arg) {
- _callback(name, addr, arg);
- }
-
- void callback() {
- _callback(hostname(), addr(), nullptr);
- }
-
- private:
- String _hostname;
- Callback _callback;
- ip_addr_t _addr { IPADDR_NONE };
- };
-
- using TaskPtr = std::unique_ptr<Task>;
- TaskPtr task;
-
- void callback(const char* name, const ip_addr_t* addr, void* arg) {
- if (task) {
- task->callback(name, addr, arg);
- }
- task.reset();
- }
-
- } // namespace internal
-
- bool started() {
- return static_cast<bool>(internal::task);
- }
-
- void start(String&& hostname, Callback&& callback) {
- auto task = std::make_unique<internal::Task>(std::move(hostname), std::move(callback));
-
- switch (dns_gethostbyname(task->hostname(), task->addr(), internal::callback, nullptr)) {
- case ERR_OK:
- task->callback();
- break;
- case ERR_INPROGRESS:
- internal::task = std::move(task);
- break;
- default:
- break;
- }
- }
-
- } // namespace dns
-
- void _terminalInitCommands() {
-
- terminalRegisterCommand(F("COMMANDS"), _terminalHelpCommand);
- terminalRegisterCommand(F("HELP"), _terminalHelpCommand);
-
- terminalRegisterCommand(F("ERASE.CONFIG"), [](const terminal::CommandContext&) {
- terminalOK();
- customResetReason(CustomResetReason::Terminal);
- eraseSDKConfig();
- *((int*) 0) = 0; // see https://github.com/esp8266/Arduino/issues/1494
- });
-
- terminalRegisterCommand(F("ADC"), [](const terminal::CommandContext& ctx) {
- const int pin = (ctx.argc == 2)
- ? ctx.argv[1].toInt()
- : A0;
-
- ctx.output.println(analogRead(pin));
- terminalOK(ctx);
- });
-
- terminalRegisterCommand(F("GPIO"), [](const terminal::CommandContext& ctx) {
- const int pin = (ctx.argc >= 2)
- ? ctx.argv[1].toInt()
- : -1;
-
- if ((pin >= 0) && !gpioValid(pin)) {
- terminalError(ctx, F("Invalid pin number"));
- return;
- }
-
- int start = 0;
- int end = gpioPins();
-
- switch (ctx.argc) {
- case 3:
- pinMode(pin, OUTPUT);
- digitalWrite(pin, (1 == ctx.argv[2].toInt()));
- break;
- case 2:
- start = pin;
- end = pin + 1;
- // fallthrough into print
- case 1:
- for (auto current = start; current < end; ++current) {
- if (gpioValid(current)) {
- ctx.output.printf_P(PSTR("%c %s @ GPIO%02d (%s)\n"),
- gpioLocked(current) ? '*' : ' ',
- GPEP(current) ? "OUTPUT" : "INPUT ",
- current,
- (HIGH == digitalRead(current)) ? "HIGH" : "LOW"
- );
- }
- }
- break;
- }
-
- terminalOK(ctx);
- });
-
- terminalRegisterCommand(F("HEAP"), [](const terminal::CommandContext& ctx) {
- static auto initial = systemInitialFreeHeap();
-
- auto stats = systemHeapStats();
- ctx.output.printf_P(PSTR("initial: %u, available: %u, fragmentation: %hhu%%\n"),
- initial, stats.available, stats.frag_pct);
-
- terminalOK(ctx);
- });
-
- terminalRegisterCommand(F("STACK"), [](const terminal::CommandContext& ctx) {
- ctx.output.printf_P(PSTR("continuation stack initial: %d, free: %u\n"),
- CONT_STACKSIZE, systemFreeStack());
- terminalOK(ctx);
- });
-
- terminalRegisterCommand(F("INFO"), [](const terminal::CommandContext&) {
- info();
- terminalOK();
- });
-
- terminalRegisterCommand(F("RESET"), [](const terminal::CommandContext& ctx) {
- if (ctx.argc == 2) {
- auto arg = ctx.argv[1].toInt();
- if (arg < SYSTEM_CHECK_MAX) {
- systemStabilityCounter(arg);
- }
- }
-
- terminalOK(ctx);
- deferredReset(100, CustomResetReason::Terminal);
- });
-
- terminalRegisterCommand(F("UPTIME"), [](const terminal::CommandContext& ctx) {
- ctx.output.println(getUptime());
- terminalOK(ctx);
- });
-
- #if SECURE_CLIENT == SECURE_CLIENT_BEARSSL
- terminalRegisterCommand(F("MFLN.PROBE"), [](const terminal::CommandContext& ctx) {
- if (ctx.argc != 3) {
- terminalError(F("[url] [value]"));
- return;
- }
-
- URL _url(ctx.argv[1]);
- uint16_t requested_mfln = atol(ctx.argv[2].c_str());
-
- auto client = std::make_unique<BearSSL::WiFiClientSecure>();
- client->setInsecure();
-
- if (client->probeMaxFragmentLength(_url.host.c_str(), _url.port, requested_mfln)) {
- terminalOK();
- } else {
- terminalError(F("Buffer size not supported"));
- }
- });
- #endif
-
- terminalRegisterCommand(F("HOST"), [](const terminal::CommandContext& ctx) {
- if (ctx.argc != 2) {
- terminalError(ctx, F("HOST <hostname>"));
- return;
- }
-
- dns::start(String(ctx.argv[1]), [&](const char* name, const ip_addr_t* addr, void*) {
- if (!addr) {
- ctx.output.printf_P(PSTR("%s not found\n"), name);
- return;
- }
-
- ctx.output.printf_P(PSTR("%s has address %s\n"),
- name, IPAddress(addr).toString().c_str());
- });
-
- while (dns::started()) {
- delay(100);
- }
- });
-
- terminalRegisterCommand(F("NETSTAT"), [](const terminal::CommandContext& ctx) {
- auto print = [](Print& out, tcp_pcb* list) {
- for (tcp_pcb* pcb = list; pcb != nullptr; pcb = pcb->next) {
- out.printf_P(PSTR("state %s local %s:%hu remote %s:%hu\n"),
- tcp_debug_state_str(pcb->state),
- IPAddress(pcb->local_ip).toString().c_str(),
- pcb->local_port,
- IPAddress(pcb->remote_ip).toString().c_str(),
- pcb->remote_port);
- }
- };
-
- print(ctx.output, tcp_active_pcbs);
- print(ctx.output, tcp_tw_pcbs);
- print(ctx.output, tcp_bound_pcbs);
- });
- }
-
- void _terminalLoop() {
-
- #if DEBUG_SERIAL_SUPPORT
- while (DEBUG_PORT.available()) {
- _io.inject(DEBUG_PORT.read());
- }
- #endif
-
- _terminal.process([](terminal::Terminal::Result result) {
- bool out = false;
- switch (result) {
- case terminal::Terminal::Result::CommandNotFound:
- terminalError(terminalDefaultStream(), F("Command not found"));
- out = true;
- break;
- case terminal::Terminal::Result::BufferOverflow:
- terminalError(terminalDefaultStream(), F("Command line buffer overflow"));
- out = true;
- break;
- case terminal::Terminal::Result::Command:
- out = true;
- break;
- case terminal::Terminal::Result::Pending:
- out = false;
- break;
- case terminal::Terminal::Result::Error:
- terminalError(terminalDefaultStream(), F("Unexpected error when parsing command line"));
- out = false;
- break;
- case terminal::Terminal::Result::NoInput:
- out = false;
- break;
- }
- return out;
- });
-
- #if SERIAL_RX_ENABLED
-
- while (SERIAL_RX_PORT.available() > 0) {
- char rc = SERIAL_RX_PORT.read();
- _serial_rx_buffer[_serial_rx_pointer++] = rc;
- if ((_serial_rx_pointer == SerialRxBufferSize) || (rc == 10)) {
- terminalInject(_serial_rx_buffer, (size_t) _serial_rx_pointer);
- _serial_rx_pointer = 0;
- }
- }
-
- #endif // SERIAL_RX_ENABLED
-
- }
-
- #if MQTT_SUPPORT && TERMINAL_MQTT_SUPPORT
-
- void _terminalMqttSetup() {
-
- mqttRegister([](unsigned int type, const char * topic, const char * payload) {
- if (type == MQTT_CONNECT_EVENT) {
- mqttSubscribe(MQTT_TOPIC_CMD);
- return;
- }
-
- if (type == MQTT_MESSAGE_EVENT) {
- String t = mqttMagnitude((char *) topic);
- if (!t.startsWith(MQTT_TOPIC_CMD)) return;
- if (!strlen(payload)) return;
-
- String cmd(payload);
- if (!cmd.endsWith("\r\n") && !cmd.endsWith("\n")) {
- cmd += '\n';
- }
-
- // TODO: unlike http handler, we have only one output stream
- // and **must** have a fixed-size output buffer
- // (wishlist: MQTT client does some magic and we don't buffer twice)
- schedule_function([cmd]() {
- PrintString buffer(TCP_MSS);
- StreamAdapter<const char*> stream(buffer, cmd.c_str(), cmd.c_str() + cmd.length() + 1);
-
- String out;
- terminal::Terminal handler(stream);
- switch (handler.processLine()) {
- case terminal::Terminal::Result::CommandNotFound:
- out += F("Command not found");
- break;
- case terminal::Terminal::Result::Command:
- out = std::move(buffer);
- default:
- break;
- }
-
- if (out.length()) {
- mqttSendRaw(mqttTopic(MQTT_TOPIC_CMD, false).c_str(), out.c_str(), false);
- }
- });
-
- return;
- }
- });
-
- }
-
- #endif // MQTT_SUPPORT && TERMINAL_MQTT_SUPPORT
-
- } // namespace
-
- // -----------------------------------------------------------------------------
- // Pubic API
- // -----------------------------------------------------------------------------
-
- #if TERMINAL_WEB_API_SUPPORT
-
- // XXX: new `apiRegister()` depends that `webServer()` is available, meaning we can't call this setup func
- // before the `webSetup()` is called. ATM, just make sure it is in order.
-
- void terminalWebApiSetup() {
- #if API_SUPPORT
- apiRegister(getSetting("termWebApiPath", TERMINAL_WEB_API_PATH),
- [](ApiRequest& api) {
- api.handle([](AsyncWebServerRequest* request) {
- AsyncResponseStream *response = request->beginResponseStream("text/plain");
- for (auto* name : _terminal.names()) {
- response->print(name);
- response->print("\r\n");
- }
-
- request->send(response);
- });
- return true;
- },
- [](ApiRequest& api) {
- // TODO: since HTTP spec allows query string to contain repeating keys, allow iteration
- // over every received 'line' to provide a way to call multiple commands at once
- auto cmd = api.param(F("line"));
- if (!cmd.length()) {
- return false;
- }
-
- if (!cmd.endsWith("\r\n") && !cmd.endsWith("\n")) {
- cmd += '\n';
- }
-
- api.handle([&](AsyncWebServerRequest* request) {
- AsyncWebPrint::scheduleFromRequest(request, [cmd](Print& print) {
- StreamAdapter<const char*> stream(print, cmd.c_str(), cmd.c_str() + cmd.length() + 1);
- terminal::Terminal handler(stream);
- handler.processLine();
- });
- });
-
- return true;
- }
- );
- #else
- webRequestRegister([](AsyncWebServerRequest* request) {
- String path(F(API_BASE_PATH));
- path += getSetting("termWebApiPath", TERMINAL_WEB_API_PATH);
- if (path != request->url()) {
- return false;
- }
-
- if (!apiAuthenticate(request)) {
- request->send(403);
- return true;
- }
-
- auto* cmd_param = request->getParam("line", (request->method() == HTTP_PUT));
- if (!cmd_param) {
- request->send(500);
- return true;
- }
-
- auto cmd = cmd_param->value();
- if (!cmd.length()) {
- request->send(500);
- return true;
- }
-
- if (!cmd.endsWith("\r\n") && !cmd.endsWith("\n")) {
- cmd += '\n';
- }
-
- // TODO: batch requests? processLine() -> process(...)
- AsyncWebPrint::scheduleFromRequest(request, [cmd](Print& print) {
- StreamAdapter<const char*> stream(print, cmd.c_str(), cmd.c_str() + cmd.length() + 1);
- terminal::Terminal handler(stream);
- handler.processLine();
- });
-
- return true;
- });
- #endif // API_SUPPORT
- }
-
- #endif // TERMINAL_WEB_API_SUPPORT
-
- Stream & terminalDefaultStream() {
- return (Stream &) _io;
- }
-
- size_t terminalCapacity() {
- return _io.capacity();
- }
-
- void terminalInject(void *data, size_t len) {
- _io.inject((char *) data, len);
- }
-
- void terminalInject(char ch) {
- _io.inject(ch);
- }
-
- void terminalRegisterCommand(const __FlashStringHelper* name, terminal::Terminal::CommandFunc func) {
- terminal::Terminal::addCommand(name, func);
- };
-
- void terminalOK(Print& print) {
- print.print(F("+OK\n"));
- }
-
- void terminalError(Print& print, const String& error) {
- print.printf("-ERROR: %s\n", error.c_str());
- }
-
- void terminalOK(const terminal::CommandContext& ctx) {
- terminalOK(ctx.output);
- }
-
- void terminalError(const terminal::CommandContext& ctx, const String& error) {
- terminalError(ctx.output, error);
- }
-
- void terminalOK() {
- terminalOK(_io);
- }
-
- void terminalError(const String& error) {
- terminalError(_io, error);
- }
-
- void terminalSetup() {
-
- // Show DEBUG panel with input
- #if WEB_SUPPORT
- wsRegister()
- .onVisible([](JsonObject& root) { root["cmdVisible"] = 1; });
- #endif
-
- // Similar to the above, but we allow only very small and in-place outputs.
- #if MQTT_SUPPORT && TERMINAL_MQTT_SUPPORT
- _terminalMqttSetup();
- #endif
-
- // Initialize default commands
- _terminalInitCommands();
-
- #if SERIAL_RX_ENABLED
- SERIAL_RX_PORT.begin(SERIAL_RX_BAUDRATE);
- #endif // SERIAL_RX_ENABLED
-
- // Register loop
- espurnaRegisterLoop(_terminalLoop);
-
- }
-
- #endif // TERMINAL_SUPPORT
-
|