IR blaster with esp32, WIP
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
esp-irblaster/main/console/telnet_parser.c

540 lines
18 KiB

//#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#include <stddef.h>
#include <stdint.h>
#include <sys/queue.h>
#include <malloc.h>
#include <stdbool.h>
#include <esp_log.h>
#include <linenoise/linenoise.h>
#include <esp_system.h>
#include "telnet_parser.h"
#include "console_server.h"
static const char *TAG = "telnet";
// The console is all single-threaded, using a circular buffer, so we can get away
// with a global state here
enum telnet_code {
CODE_SE = 240,
CODE_NOP = 241,
CODE_DATA_MARK = 242,
CODE_BRK = 243, // break signal - kick all clients (including yourself)
CODE_IP = 244, // interrupt process (reboots ESP)
CODE_AO = 245, // abort output
CODE_AYT = 246, // are you there? (sends an ASCII text reply)
CODE_ERASE_CHAR = 247, // these are obsolete
CODE_ERASE_LINE = 248,
CODE_GO_AHEAD = 249, // not used, always negotiate to SGA
CODE_SB = 250,
CODE_WILL = 251,
CODE_WONT = 252,
CODE_DO = 253,
CODE_DONT = 254,
CODE_IAC = 255
};
enum parser_state {
// next character is expected to be text or IAC
PS_TEXT = 0,
// received IAC, next character should be a verb
PS_IAC,
PS_WILL,
PS_WONT,
PS_DO,
PS_DONT,
PS_SUBNEG,
PS_SUBNEG_BODY,
PS_SUBNEG_BODY_IAC
};
struct awaiting_reply_item;
struct awaiting_reply_item {
enum telnet_code code;
enum telnet_option option;
SLIST_ENTRY(awaiting_reply_item) next;
};
SLIST_HEAD(awaiting_reply_list, awaiting_reply_item);
#define SUBNEG_BUF_LEN 16
static struct telnet_state {
enum parser_state pstate;
enum telnet_option subneg_option;
struct awaiting_reply_list awaiting_reply;
uint8_t subneg_buf[SUBNEG_BUF_LEN];
uint8_t subneg_buf_i;
bool mode_rx_binary;
bool mode_tx_binary;
} s_ts = { /* all zeros */ };
static void _expect_reply(enum telnet_code code, enum telnet_option option)
{
ESP_LOGV(TAG, "Awaiting reply to %d:0x%02x", code, option);
struct awaiting_reply_item *item = calloc(sizeof(struct awaiting_reply_item), 1);
item->code = code;
item->option = option;
SLIST_INSERT_HEAD(&s_ts.awaiting_reply, item, next);
}
static void send_will(console_ctx_t *cctx, enum telnet_option opt, bool is_response)
{
ESP_LOGD(TAG, "Send WILL 0x%02x", opt);
if (!is_response) _expect_reply(CODE_WILL, opt);
const uint8_t buf[3] = {CODE_IAC, CODE_WILL, opt};
console_write_ctx(cctx, (const char *) buf, 3);
}
static void send_wont(console_ctx_t *cctx, enum telnet_option opt, bool is_response)
{
ESP_LOGD(TAG, "Send WON'T 0x%02x", opt);
if (!is_response) _expect_reply(CODE_WONT, opt);
const uint8_t buf[3] = {CODE_IAC, CODE_WONT, opt};
console_write_ctx(cctx, (const char *) buf, 3);
}
static void send_do(console_ctx_t *cctx, enum telnet_option opt, bool is_response)
{
ESP_LOGD(TAG, "Send DO 0x%02x", opt);
if (!is_response) _expect_reply(CODE_DO, opt);
const uint8_t buf[3] = {CODE_IAC, CODE_DO, opt};
console_write_ctx(cctx, (const char *) buf, 3);
}
static void send_dont(console_ctx_t *cctx, enum telnet_option opt, bool is_response)
{
ESP_LOGD(TAG, "Send DON'T 0x%02x", opt);
if (!is_response) _expect_reply(CODE_DONT, opt);
const uint8_t buf[3] = {CODE_IAC, CODE_DONT, opt};
console_write_ctx(cctx, (const char *) buf, 3);
}
static void send_subnegotiate(console_ctx_t *cctx, enum telnet_option opt, const uint8_t *data, size_t len, bool is_response)
{
if (!is_response) _expect_reply(CODE_SB, opt);
ESP_LOGD(TAG, "Send SUBNEG 0x%02x", opt);
const uint8_t hdr[] = {CODE_IAC, CODE_SB, opt};
console_write_ctx(cctx, (const char *) hdr, 3);
// a heroic attempt at doing the double-IAC encoding without malloc
const uint8_t *last_chunk = data;
const uint8_t a_iac = CODE_IAC;
for (int i = 0; i < len; i++) {
if (data[i] == CODE_IAC) {
if (last_chunk != &data[i]) {
console_write_ctx(cctx, (const char *) last_chunk, &data[i] - last_chunk);
console_write_ctx(cctx, (const char *) &a_iac, 1);
last_chunk = &data[i+1];
}
}
}
if (last_chunk != &data[len]) {
console_write_ctx(cctx, (const char *) last_chunk, &data[len] - last_chunk);
}
const uint8_t foot[] = {CODE_IAC, CODE_SE};
console_write_ctx(cctx, (const char *) foot, 2);
}
void telnet_send_subnegotiate(console_ctx_t *cctx, enum telnet_option opt, const uint8_t *data, size_t len)
{
send_subnegotiate(cctx, opt, data, len, false);
}
void telnet_send_will(console_ctx_t *cctx, enum telnet_option opt)
{
send_will(cctx, opt, false);
}
void telnet_send_wont(console_ctx_t *cctx, enum telnet_option opt)
{
send_wont(cctx, opt, false);
}
void telnet_send_do(console_ctx_t *cctx, enum telnet_option opt)
{
send_do(cctx, opt, false);
}
void telnet_send_dont(console_ctx_t *cctx, enum telnet_option opt)
{
send_dont(cctx, opt, false);
}
/**
* Peer agreed to DO something,
* or announces it WILL do something
*/
static void handle_will(console_ctx_t *cctx, enum telnet_option opt)
{
struct awaiting_reply_item *it;
SLIST_FOREACH(it, &s_ts.awaiting_reply, next) {
if (it->option == opt) {
if (it->code == CODE_DO) {
ESP_LOGD(TAG, "Peer agreed to DO 0x%02x", opt);
} else if (it->code == CODE_DONT) {
ESP_LOGW(TAG, "Peer refused to NOT DO 0x%02x", opt);
// TODO deal with it
} else {
continue; // leave this entry alone, it's not related
}
SLIST_REMOVE(&s_ts.awaiting_reply, it, awaiting_reply_item, next);
free(it);
return;
}
}
// unexpected WILL - peer offers to enable an option
switch (opt) {
case OPT_BINARY:
send_do(cctx, opt, true);
s_ts.mode_rx_binary = true;
break;
case OPT_ECHO:
send_dont(cctx, opt, true);
// that's a bad idea, don't do it
break;
case OPT_SUPPRESS_GO_AHEAD:
send_do(cctx, opt, true);
break;
default:
ESP_LOGD(TAG, "unknown option for WILL: 0x%02x", opt);
// please don't, we don't understand what it is
send_dont(cctx, opt, true);
}
}
/**
* Peer refused to DO something,
* or announces it WON'T do something
*/
static void handle_wont(console_ctx_t *cctx, enum telnet_option opt)
{
struct awaiting_reply_item *it;
SLIST_FOREACH(it, &s_ts.awaiting_reply, next) {
if (it->option == opt) {
if (it->code == CODE_DO) {
ESP_LOGW(TAG, "Peer refused to DO 0x%02x", opt);
// TODO deal with it
} else if (it->code == CODE_DONT) {
ESP_LOGD(TAG, "Peer agreed to NOT DO 0x%02x", opt);
} else {
continue; // leave this entry alone, it's not related
}
SLIST_REMOVE(&s_ts.awaiting_reply, it, awaiting_reply_item, next);
free(it);
return;
}
}
// unexpected WON'T - peer offers to disable an option
switch (opt) {
case OPT_BINARY:
send_dont(cctx, opt, true);
s_ts.mode_rx_binary = false;
break;
case OPT_ECHO:
send_dont(cctx, opt, true);
// OK, we don't care
break;
case OPT_SUPPRESS_GO_AHEAD:
// We want SGA, GA is some legacy nonsense nobody uses
send_do(cctx, opt, true);
break;
// TODO handle supported options
default:
ESP_LOGD(TAG, "unknown option for WON'T: 0x%02x", opt);
// yeah sure, don't do it, whatever
send_dont(cctx, opt, true);
}
}
/**
* Peer requests we DO something,
* or agrees to something we announced with WILL
*/
static void handle_do(console_ctx_t *cctx, enum telnet_option opt)
{
struct awaiting_reply_item *it;
SLIST_FOREACH(it, &s_ts.awaiting_reply, next) {
if (it->option == opt) {
if (it->code == CODE_WONT) {
ESP_LOGW(TAG, "Peer rejected that we WON'T 0x%02x", opt);
// TODO deal with it
} else if (it->code == CODE_WILL) {
ESP_LOGD(TAG, "Peer accepted that we WILL 0x%02x", opt);
} else {
continue; // leave this entry alone, it's not related
}
SLIST_REMOVE(&s_ts.awaiting_reply, it, awaiting_reply_item, next);
free(it);
return;
}
}
// unexpected DO - peer requests we do something
switch (opt) {
case OPT_BINARY:
send_will(cctx, opt, true);
s_ts.mode_tx_binary = true;
break;
case OPT_ECHO:
// FIXME
send_wont(cctx, opt, true);
// send_will(cctx, opt, true);
// linenoiseSetEchoMode(1);
break;
case OPT_SUPPRESS_GO_AHEAD:
send_will(cctx, opt, true);
break;
case OPT_STATUS:
send_will(cctx, opt, true);
break;
case OPT_TIMING_MARK:
send_will(cctx, opt, true);
break;
// TODO handle supported options
default:
ESP_LOGD(TAG, "unknown option for DO: 0x%02x", opt);
// we don't know how to do that, so we won't
send_wont(cctx, opt, true);
}
}
/**
* Peer requests we DON'T do something,
* or agrees we shouldn't do something we announced for with WON'T
*/
static void handle_dont(console_ctx_t *cctx, enum telnet_option opt)
{
struct awaiting_reply_item *it;
SLIST_FOREACH(it, &s_ts.awaiting_reply, next) {
if (it->option == opt) {
if (it->code == CODE_WILL) {
ESP_LOGW(TAG, "Peer rejected that we WILL 0x%02x", opt);
// TODO deal with it
switch (opt) {
case OPT_ECHO:
// ok, let's not echo then
// FIXME
// linenoiseSetEchoMode(0);
break;
default:
ESP_LOGW(TAG, "Rejection unhandled");
}
} else if (it->code == CODE_WONT) {
ESP_LOGD(TAG, "Peer accepted that we WON'T 0x%02x", opt);
} else {
continue; // leave this entry alone, it's not related
}
SLIST_REMOVE(&s_ts.awaiting_reply, it, awaiting_reply_item, next);
free(it);
return;
}
}
// unexpected DON'T - peer requests we don't do something
switch (opt) {
case OPT_BINARY:
send_wont(cctx, opt, true);
s_ts.mode_tx_binary = false;
break;
case OPT_ECHO:
send_wont(cctx, opt, true);
// FIXME
// linenoiseSetEchoMode(0);
break;
case OPT_SUPPRESS_GO_AHEAD:
// no, we absolutely won't use GA
send_will(cctx, opt, true);
break;
// TODO handle supported options
default:
ESP_LOGD(TAG, "unknown option for DON'T: 0x%02x", opt);
// false is probably the default, so we won't do that, ok...
send_wont(cctx, opt, true);
}
}
/**
* subnegotiation command (ts.subneg_option) was received and is now stored in
* ts.subneg_buf[0 .. ts.subneg_buf_i]. Handle it accordingly
*/
static void handle_subnegotiate(void)
{
struct awaiting_reply_item *it;
SLIST_FOREACH(it, &s_ts.awaiting_reply, next) {
if (it->code == CODE_SB && it->option == s_ts.subneg_option) {
// this is an expected reply, delete the await token
SLIST_REMOVE(&s_ts.awaiting_reply, it, awaiting_reply_item, next);
ESP_LOGD(TAG, "got reply to SB 0x%02x", s_ts.subneg_option);
// TODO now we would handle the received data
free(it);
return;
}
}
// it was not a reply to our request, i.e. the client is asking for something
ESP_LOGW(TAG, "recv unsupported subneg request 0x%02x", s_ts.subneg_option);
// TODO handle some requests..
}
size_t telnet_middleware_read(console_ctx_t *cctx, uint8_t *buffer, size_t len)
{
ESP_LOGV(TAG, "-- Telnet handle %d chars --", len);
ESP_LOG_BUFFER_HEX_LEVEL(TAG, buffer, (uint16_t) len, ESP_LOG_VERBOSE);
uint8_t *wp = buffer;
for (size_t i = 0; i < len; i++) {
uint8_t c = buffer[i];
if (c >= 32 && c < 127) {
ESP_LOGV(TAG, "char %d (%c)", c, c);
} else {
ESP_LOGV(TAG, "char %d", c);
}
switch (s_ts.pstate) {
case PS_TEXT:
if (c == CODE_IAC) {
ESP_LOGV(TAG, "TEXT->IAC");
s_ts.pstate = PS_IAC;
} else {
ESP_LOGV(TAG, "TEXT(%c)", c);
*wp++ = c;
}
break;
case PS_IAC:
switch (c) {
case CODE_IAC:
// double IAC means literal IAC (but this shouldn't normally happen)
*wp++ = c;
s_ts.pstate = PS_TEXT;
ESP_LOGV(TAG, "Double IAC in TEXT");
break;
case CODE_WILL:
ESP_LOGV(TAG, "IAC->WILL");
s_ts.pstate = PS_WILL;
break;
case CODE_WONT:
ESP_LOGV(TAG, "IAC->WONT");
s_ts.pstate = PS_WONT;
break;
case CODE_DO:
ESP_LOGV(TAG, "IAC->DO");
s_ts.pstate = PS_DO;
break;
case CODE_DONT:
ESP_LOGV(TAG, "IAC->DONT");
s_ts.pstate = PS_DONT;
break;
case CODE_SB:
ESP_LOGV(TAG, "IAC->SUBNEG");
s_ts.pstate = PS_SUBNEG;
break;
case CODE_AYT:
ESP_LOGV(TAG, "IAC->AYT");
s_ts.pstate = PS_TEXT;
const char *reply = "\r\nI am here.\r\n";
console_write_ctx(cctx, reply, strlen(reply));
break;
case CODE_IP:
ESP_LOGV(TAG, "IAC->IP, reboot");
esp_restart();
break;
case CODE_BRK:
ESP_LOGV(TAG, "IAC->BRK, kick clients");
telnetsrv_kick_all();
break;
default:
ESP_LOGW(TAG, "Unexpected code IAC+%d, discard!", c);
// Unecpected character, discard and go to base state
s_ts.pstate = PS_TEXT;
}
break;
case PS_WILL:
handle_will(cctx, c);
s_ts.pstate = PS_TEXT;
ESP_LOGV(TAG, "WILL->TEXT");
break;
case PS_WONT:
handle_wont(cctx, c);
s_ts.pstate = PS_TEXT;
ESP_LOGV(TAG, "WONT->TEXT");
break;
case PS_DO:
handle_do(cctx, c);
s_ts.pstate = PS_TEXT;
ESP_LOGV(TAG, "DO->TEXT");
break;
case PS_DONT:
handle_dont(cctx, c);
s_ts.pstate = PS_TEXT;
ESP_LOGV(TAG, "DONT->TEXT");
break;
case PS_SUBNEG:
s_ts.subneg_option = c;
s_ts.pstate = PS_SUBNEG_BODY;
s_ts.subneg_buf_i = 0;
ESP_LOGV(TAG, "SUBNEG->SUBNEG_BODY, opt %d", c);
break;
case PS_SUBNEG_BODY:
if (c == CODE_IAC) {
ESP_LOGV(TAG, "IAC in subneg body");
s_ts.pstate = PS_SUBNEG_BODY_IAC;
} else {
if (s_ts.subneg_buf_i < SUBNEG_BUF_LEN) {
ESP_LOGV(TAG, "subneg += %d", c);
s_ts.subneg_buf[s_ts.subneg_buf_i++] = c;
} else {
ESP_LOGV(TAG, "subneg too long!");
}
}
break;
case PS_SUBNEG_BODY_IAC:
switch (c) {
case CODE_IAC:
ESP_LOGV(TAG, "Literal IAC in subneg");
// double IAC means literal IAC code
s_ts.subneg_buf[s_ts.subneg_buf_i++] = CODE_IAC;
s_ts.pstate = PS_SUBNEG_BODY;
break;
case CODE_SE:
ESP_LOGV(TAG, "Subneg END");
// end of subneg block
if (s_ts.subneg_buf_i == SUBNEG_BUF_LEN) {
ESP_LOGW(TAG, "Subneg buf OV, discard command SB %d!", s_ts.subneg_option);
} else {
ESP_LOGV(TAG, "Subneg OK! Handling...");
handle_subnegotiate();
}
s_ts.pstate = PS_TEXT;
break;
default:
ESP_LOGV(TAG, "Illegal char in subneg IAC+%d, discard", c);
// illegal character, act like the subneg block was closed, but discard the content
s_ts.pstate = PS_TEXT;
}
break;
}
}
size_t remains = wp - buffer;
ESP_LOGV(TAG, "Remain %d cleartext chars", remains);
ESP_LOGV(TAG, "> %*.s", remains, buffer);
return remains;
}