2b5723d455
Each backend can configure separately features like colors on/off, VT100 handling and so on. This can be very handy for planned MQTT backend. Shell will not send VT100 commands when VT100 is not enabled globally or for particular backend. Signed-off-by: Jakub Rzeszutko <jakub.rzeszutko@nordicsemi.no>
510 lines
11 KiB
C
510 lines
11 KiB
C
/*
|
|
* Copyright (c) 2017 Intel Corporation
|
|
* Copyright (c) 2019 Nordic Semiconductor ASA
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#include <init.h>
|
|
|
|
#include <logging/log.h>
|
|
#include <net/net_context.h>
|
|
#include <net/net_ip.h>
|
|
#include <net/net_pkt.h>
|
|
#include <shell/shell_telnet.h>
|
|
|
|
#include "shell_telnet_protocol.h"
|
|
|
|
SHELL_TELNET_DEFINE(shell_transport_telnet);
|
|
SHELL_DEFINE(shell_telnet, CONFIG_SHELL_PROMPT_TELNET, &shell_transport_telnet,
|
|
CONFIG_SHELL_TELNET_LOG_MESSAGE_QUEUE_SIZE,
|
|
CONFIG_SHELL_TELNET_LOG_MESSAGE_QUEUE_TIMEOUT,
|
|
SHELL_FLAG_OLF_CRLF);
|
|
|
|
LOG_MODULE_REGISTER(shell_telnet, CONFIG_SHELL_TELNET_LOG_LEVEL);
|
|
|
|
struct shell_telnet *sh_telnet;
|
|
|
|
/* Various definitions mapping the TELNET service configuration options */
|
|
#define TELNET_PORT CONFIG_SHELL_TELNET_PORT
|
|
#define TELNET_LINE_SIZE CONFIG_SHELL_TELNET_LINE_BUF_SIZE
|
|
#define TELNET_TIMEOUT CONFIG_SHELL_TELNET_SEND_TIMEOUT
|
|
|
|
#define TELNET_MIN_COMMAND_LEN 2
|
|
|
|
/* Basic TELNET implmentation. */
|
|
|
|
static void telnet_end_client_connection(void)
|
|
{
|
|
struct net_pkt *pkt;
|
|
|
|
(void)net_context_put(sh_telnet->client_ctx);
|
|
|
|
sh_telnet->client_ctx = NULL;
|
|
sh_telnet->output_lock = false;
|
|
|
|
k_work_cancel_delayable_sync(&sh_telnet->send_work,
|
|
&sh_telnet->work_sync);
|
|
|
|
/* Flush the RX FIFO */
|
|
while ((pkt = k_fifo_get(&sh_telnet->rx_fifo, K_NO_WAIT)) != NULL) {
|
|
net_pkt_unref(pkt);
|
|
}
|
|
}
|
|
|
|
static void telnet_sent_cb(struct net_context *client,
|
|
int status, void *user_data)
|
|
{
|
|
if (status < 0) {
|
|
telnet_end_client_connection();
|
|
LOG_ERR("Could not send packet %d", status);
|
|
}
|
|
}
|
|
|
|
static void telnet_command_send_reply(uint8_t *msg, uint16_t len)
|
|
{
|
|
int err;
|
|
|
|
if (sh_telnet->client_ctx == NULL) {
|
|
return;
|
|
}
|
|
|
|
err = net_context_send(sh_telnet->client_ctx, msg, len, telnet_sent_cb,
|
|
K_FOREVER, NULL);
|
|
if (err < 0) {
|
|
LOG_ERR("Failed to send command %d, shutting down", err);
|
|
telnet_end_client_connection();
|
|
}
|
|
}
|
|
|
|
static void telnet_reply_ay_command(void)
|
|
{
|
|
static const char alive[] = "Zephyr at your service\r\n";
|
|
|
|
telnet_command_send_reply((uint8_t *)alive, strlen(alive));
|
|
}
|
|
|
|
static void telnet_reply_do_command(struct telnet_simple_command *cmd)
|
|
{
|
|
switch (cmd->opt) {
|
|
case NVT_OPT_SUPR_GA:
|
|
cmd->op = NVT_CMD_WILL;
|
|
break;
|
|
default:
|
|
cmd->op = NVT_CMD_WONT;
|
|
break;
|
|
}
|
|
|
|
telnet_command_send_reply((uint8_t *)cmd,
|
|
sizeof(struct telnet_simple_command));
|
|
}
|
|
|
|
static void telnet_reply_command(struct telnet_simple_command *cmd)
|
|
{
|
|
if (!cmd->iac) {
|
|
return;
|
|
}
|
|
|
|
switch (cmd->op) {
|
|
case NVT_CMD_AO:
|
|
/* OK, no output then */
|
|
sh_telnet->output_lock = true;
|
|
sh_telnet->line_out.len = 0;
|
|
k_work_cancel_delayable_sync(&sh_telnet->send_work,
|
|
&sh_telnet->work_sync);
|
|
break;
|
|
case NVT_CMD_AYT:
|
|
telnet_reply_ay_command();
|
|
break;
|
|
case NVT_CMD_DO:
|
|
telnet_reply_do_command(cmd);
|
|
break;
|
|
default:
|
|
LOG_DBG("Operation %u not handled", cmd->op);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int telnet_send(void)
|
|
{
|
|
int err;
|
|
|
|
if (sh_telnet->line_out.len == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (sh_telnet->client_ctx == NULL) {
|
|
return -ENOTCONN;
|
|
}
|
|
|
|
err = net_context_send(sh_telnet->client_ctx, sh_telnet->line_out.buf,
|
|
sh_telnet->line_out.len, telnet_sent_cb,
|
|
K_FOREVER, NULL);
|
|
if (err < 0) {
|
|
LOG_ERR("Failed to send %d, shutting down", err);
|
|
telnet_end_client_connection();
|
|
return err;
|
|
}
|
|
|
|
/* We reinitialize the line buffer */
|
|
sh_telnet->line_out.len = 0;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void telnet_send_prematurely(struct k_work *work)
|
|
{
|
|
(void)telnet_send();
|
|
}
|
|
|
|
static inline bool telnet_handle_command(struct net_pkt *pkt)
|
|
{
|
|
/* Commands are two or three bytes. */
|
|
NET_PKT_DATA_ACCESS_CONTIGUOUS_DEFINE(cmd_access, uint16_t);
|
|
struct telnet_simple_command *cmd;
|
|
|
|
cmd = (struct telnet_simple_command *)net_pkt_get_data(pkt,
|
|
&cmd_access);
|
|
if (!cmd || cmd->iac != NVT_CMD_IAC) {
|
|
return false;
|
|
}
|
|
|
|
if (IS_ENABLED(CONFIG_SHELL_TELNET_SUPPORT_COMMAND)) {
|
|
LOG_DBG("Got a command %u/%u/%u", cmd->iac, cmd->op, cmd->opt);
|
|
telnet_reply_command(cmd);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void telnet_recv(struct net_context *client,
|
|
struct net_pkt *pkt,
|
|
union net_ip_header *ip_hdr,
|
|
union net_proto_header *proto_hdr,
|
|
int status,
|
|
void *user_data)
|
|
{
|
|
size_t len;
|
|
|
|
if (!pkt || status) {
|
|
telnet_end_client_connection();
|
|
|
|
LOG_DBG("Telnet client dropped (AF_INET%s) status %d",
|
|
net_context_get_family(client) == AF_INET ?
|
|
"" : "6", status);
|
|
return;
|
|
}
|
|
|
|
len = net_pkt_remaining_data(pkt);
|
|
|
|
if (len >= TELNET_MIN_COMMAND_LEN) {
|
|
if (telnet_handle_command(pkt)) {
|
|
LOG_DBG("Handled command");
|
|
goto unref;
|
|
}
|
|
}
|
|
|
|
/* Fifo add */
|
|
k_fifo_put(&sh_telnet->rx_fifo, pkt);
|
|
|
|
sh_telnet->shell_handler(SHELL_TRANSPORT_EVT_RX_RDY,
|
|
sh_telnet->shell_context);
|
|
|
|
return;
|
|
|
|
unref:
|
|
net_pkt_unref(pkt);
|
|
}
|
|
|
|
static void telnet_accept(struct net_context *client,
|
|
struct sockaddr *addr,
|
|
socklen_t addrlen,
|
|
int error,
|
|
void *user_data)
|
|
{
|
|
if (error) {
|
|
LOG_ERR("Error %d", error);
|
|
goto error;
|
|
}
|
|
|
|
if (sh_telnet->client_ctx) {
|
|
LOG_INF("A telnet client is already in.");
|
|
goto error;
|
|
}
|
|
|
|
if (net_context_recv(client, telnet_recv, K_NO_WAIT, NULL)) {
|
|
LOG_ERR("Unable to setup reception (family %u)",
|
|
net_context_get_family(client));
|
|
goto error;
|
|
}
|
|
|
|
net_context_set_accepting(client, false);
|
|
|
|
LOG_DBG("Telnet client connected (family AF_INET%s)",
|
|
net_context_get_family(client) == AF_INET ? "" : "6");
|
|
|
|
sh_telnet->client_ctx = client;
|
|
|
|
return;
|
|
error:
|
|
net_context_put(client);
|
|
}
|
|
|
|
static void telnet_setup_server(struct net_context **ctx, sa_family_t family,
|
|
struct sockaddr *addr, socklen_t addrlen)
|
|
{
|
|
if (net_context_get(family, SOCK_STREAM, IPPROTO_TCP, ctx)) {
|
|
LOG_ERR("No context available");
|
|
goto error;
|
|
}
|
|
|
|
if (net_context_bind(*ctx, addr, addrlen)) {
|
|
LOG_ERR("Cannot bind on family AF_INET%s",
|
|
family == AF_INET ? "" : "6");
|
|
goto error;
|
|
}
|
|
|
|
if (net_context_listen(*ctx, 0)) {
|
|
LOG_ERR("Cannot listen on");
|
|
goto error;
|
|
}
|
|
|
|
if (net_context_accept(*ctx, telnet_accept, K_NO_WAIT, NULL)) {
|
|
LOG_ERR("Cannot accept");
|
|
goto error;
|
|
}
|
|
|
|
LOG_DBG("Telnet console enabled on AF_INET%s",
|
|
family == AF_INET ? "" : "6");
|
|
|
|
return;
|
|
error:
|
|
LOG_ERR("Unable to start telnet on AF_INET%s",
|
|
family == AF_INET ? "" : "6");
|
|
|
|
if (*ctx) {
|
|
(void)net_context_put(*ctx);
|
|
*ctx = NULL;
|
|
}
|
|
}
|
|
|
|
static int telnet_init(void)
|
|
{
|
|
if (IS_ENABLED(CONFIG_NET_IPV4)) {
|
|
struct sockaddr_in any_addr4 = {
|
|
.sin_family = AF_INET,
|
|
.sin_port = htons(TELNET_PORT),
|
|
.sin_addr = INADDR_ANY_INIT
|
|
};
|
|
static struct net_context *ctx4;
|
|
|
|
telnet_setup_server(&ctx4, AF_INET,
|
|
(struct sockaddr *)&any_addr4,
|
|
sizeof(any_addr4));
|
|
}
|
|
|
|
if (IS_ENABLED(CONFIG_NET_IPV6)) {
|
|
struct sockaddr_in6 any_addr6 = {
|
|
.sin6_family = AF_INET6,
|
|
.sin6_port = htons(TELNET_PORT),
|
|
.sin6_addr = IN6ADDR_ANY_INIT
|
|
};
|
|
static struct net_context *ctx6;
|
|
|
|
telnet_setup_server(&ctx6, AF_INET6,
|
|
(struct sockaddr *)&any_addr6,
|
|
sizeof(any_addr6));
|
|
}
|
|
|
|
LOG_INF("Telnet shell backend initialized");
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Shell API */
|
|
|
|
static int init(const struct shell_transport *transport,
|
|
const void *config,
|
|
shell_transport_handler_t evt_handler,
|
|
void *context)
|
|
{
|
|
int err;
|
|
|
|
sh_telnet = (struct shell_telnet *)transport->ctx;
|
|
|
|
err = telnet_init();
|
|
if (err != 0) {
|
|
return err;
|
|
}
|
|
|
|
memset(sh_telnet, 0, sizeof(struct shell_telnet));
|
|
|
|
sh_telnet->shell_handler = evt_handler;
|
|
sh_telnet->shell_context = context;
|
|
|
|
k_fifo_init(&sh_telnet->rx_fifo);
|
|
k_work_init_delayable(&sh_telnet->send_work, telnet_send_prematurely);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int uninit(const struct shell_transport *transport)
|
|
{
|
|
if (sh_telnet == NULL) {
|
|
return -ENODEV;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int enable(const struct shell_transport *transport, bool blocking)
|
|
{
|
|
if (sh_telnet == NULL) {
|
|
return -ENODEV;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int write(const struct shell_transport *transport,
|
|
const void *data, size_t length, size_t *cnt)
|
|
{
|
|
struct shell_telnet_line_buf *lb;
|
|
size_t copy_len;
|
|
int err;
|
|
uint32_t timeout;
|
|
bool was_running;
|
|
|
|
if (sh_telnet == NULL) {
|
|
*cnt = 0;
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (sh_telnet->client_ctx == NULL || sh_telnet->output_lock) {
|
|
*cnt = length;
|
|
return 0;
|
|
}
|
|
|
|
*cnt = 0;
|
|
lb = &sh_telnet->line_out;
|
|
|
|
/* Stop the transmission timer, so it does not interrupt the operation.
|
|
*/
|
|
timeout = k_ticks_to_ms_ceil32(
|
|
k_work_delayable_remaining_get(&sh_telnet->send_work));
|
|
was_running = k_work_cancel_delayable_sync(&sh_telnet->send_work,
|
|
&sh_telnet->work_sync);
|
|
|
|
do {
|
|
if (lb->len + length - *cnt > TELNET_LINE_SIZE) {
|
|
copy_len = TELNET_LINE_SIZE - lb->len;
|
|
} else {
|
|
copy_len = length - *cnt;
|
|
}
|
|
|
|
memcpy(lb->buf + lb->len, (uint8_t *)data + *cnt, copy_len);
|
|
lb->len += copy_len;
|
|
|
|
/* Send the data immediately if the buffer is full or line feed
|
|
* is recognized.
|
|
*/
|
|
if (lb->buf[lb->len - 1] == '\n' ||
|
|
lb->len == TELNET_LINE_SIZE) {
|
|
err = telnet_send();
|
|
if (err != 0) {
|
|
*cnt = length;
|
|
return err;
|
|
}
|
|
}
|
|
|
|
*cnt += copy_len;
|
|
} while (*cnt < length);
|
|
|
|
if (lb->len > 0) {
|
|
/* Check if the timer was already running, initialize otherwise.
|
|
*/
|
|
timeout = was_running ? timeout : TELNET_TIMEOUT;
|
|
|
|
k_work_reschedule(&sh_telnet->send_work, K_MSEC(timeout));
|
|
}
|
|
|
|
sh_telnet->shell_handler(SHELL_TRANSPORT_EVT_TX_RDY,
|
|
sh_telnet->shell_context);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int read(const struct shell_transport *transport,
|
|
void *data, size_t length, size_t *cnt)
|
|
{
|
|
struct net_pkt *pkt;
|
|
size_t read_len;
|
|
bool flush = true;
|
|
|
|
if (sh_telnet == NULL) {
|
|
return -ENODEV;
|
|
}
|
|
|
|
if (sh_telnet->client_ctx == NULL) {
|
|
goto no_data;
|
|
}
|
|
|
|
pkt = k_fifo_peek_head(&sh_telnet->rx_fifo);
|
|
if (pkt == NULL) {
|
|
goto no_data;
|
|
}
|
|
|
|
read_len = net_pkt_remaining_data(pkt);
|
|
if (read_len > length) {
|
|
read_len = length;
|
|
flush = false;
|
|
}
|
|
|
|
*cnt = read_len;
|
|
if (net_pkt_read(pkt, data, read_len) < 0) {
|
|
/* Failed to read, get rid of the faulty packet. */
|
|
LOG_ERR("Failed to read net packet.");
|
|
*cnt = 0;
|
|
flush = true;
|
|
}
|
|
|
|
if (flush) {
|
|
(void)k_fifo_get(&sh_telnet->rx_fifo, K_NO_WAIT);
|
|
net_pkt_unref(pkt);
|
|
}
|
|
|
|
return 0;
|
|
|
|
no_data:
|
|
*cnt = 0;
|
|
return 0;
|
|
}
|
|
|
|
const struct shell_transport_api shell_telnet_transport_api = {
|
|
.init = init,
|
|
.uninit = uninit,
|
|
.enable = enable,
|
|
.write = write,
|
|
.read = read
|
|
};
|
|
|
|
static int enable_shell_telnet(const struct device *arg)
|
|
{
|
|
ARG_UNUSED(arg);
|
|
|
|
bool log_backend = CONFIG_SHELL_TELNET_INIT_LOG_LEVEL > 0;
|
|
uint32_t level = (CONFIG_SHELL_TELNET_INIT_LOG_LEVEL > LOG_LEVEL_DBG) ?
|
|
CONFIG_LOG_MAX_LEVEL : CONFIG_SHELL_TELNET_INIT_LOG_LEVEL;
|
|
static const struct shell_backend_config_flags cfg_flags =
|
|
SHELL_DEFAULT_BACKEND_CONFIG_FLAGS;
|
|
|
|
return shell_init(&shell_telnet, NULL, cfg_flags, log_backend, level);
|
|
}
|
|
|
|
SYS_INIT(enable_shell_telnet, APPLICATION, 0);
|
|
|
|
const struct shell *shell_backend_telnet_get_ptr(void)
|
|
{
|
|
return &shell_telnet;
|
|
}
|