zephyr/drivers/console/telnet_console.c
Tomasz Bursztyka c5644fba20 drivers/console/telnet: Add ground support for telnet commands
This is the skeleton for such support. Let's see if this will be needed
and thus extended in the future.

For now, it's disabled by default, not advised to be enabled.
It supports AYT, AO and DO/SUPR_GA. This is very limited and only there
to show how it could be handled.

Change-Id: I736bfa23145e9b727af08db682ab001f494f8c8d
Signed-off-by: Tomasz Bursztyka <tomasz.bursztyka@linux.intel.com>
2017-01-27 12:35:53 +02:00

559 lines
12 KiB
C

/*
* Copyright (c) 2017 Intel Corporation
*
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @file
* @brief Telnet console
*
*
* Telnet console driver.
* Hooks into the printk and fputc (for printf) modules.
*
* Telnet has been standardised in 1983
* RFC 854 - https://tools.ietf.org/html/rfc854
*/
#define SYS_LOG_LEVEL CONFIG_SYS_LOG_TELNET_CONSOLE_LEVEL
#define SYS_LOG_DOMAIN "net/telnet"
#include <logging/sys_log.h>
#include <zephyr.h>
#include <init.h>
#include <misc/printk.h>
#include <console/console.h>
#include <net/buf.h>
#include <net/nbuf.h>
#include <net/net_ip.h>
#include <net/net_context.h>
#include "telnet_protocol.h"
/* Various definitions mapping the telnet service configuration options */
#define TELNET_PORT CONFIG_TELNET_CONSOLE_PORT
#define TELNET_STACK_SIZE CONFIG_TELNET_CONSOLE_THREAD_STACK
#define TELNET_PRIORITY CONFIG_TELNET_CONSOLE_PRIO
#define TELNET_LINES CONFIG_TELNET_CONSOLE_LINE_BUF_NUMBERS
#define TELNET_LINE_SIZE CONFIG_TELNET_CONSOLE_LINE_BUF_SIZE
#define TELNET_TIMEOUT K_MSEC(CONFIG_TELNET_CONSOLE_SEND_TIMEOUT)
#define TELNET_THRESHOLD CONFIG_TELNET_CONSOLE_SEND_THRESHOLD
#define TELNET_MIN_MSG 2
/* These 2 structures below are used to store the console output
* before sending it to the client. This is done to keep some
* reactivity: the ring buffer is non-protected, if first line has
* not been sent yet, and if next line is reaching the same index in rb,
* the first one will be replaced. In a perfect world, this should
* not happen. However on a loaded system with a lot of debug output
* this is bound to happen eventualy, moreover if it does not have
* the luxury to bufferize as much as it wants to. Just raise
* CONFIG_TELNET_CONSOLE_LINE_BUF_NUMBERS if possible.
*/
struct line_buf {
char buf[TELNET_LINE_SIZE];
uint16_t len;
};
struct line_buf_rb {
struct line_buf l_bufs[TELNET_LINES];
uint16_t line_in;
uint16_t line_out;
};
static struct line_buf_rb telnet_rb;
static char __noinit __stack telnet_stack[TELNET_STACK_SIZE];
static K_SEM_DEFINE(send_lock, 0, UINT_MAX);
/* The timer is used to send non-lf terminated output that has
* been around for "tool long". This will prove to be useful
* to send the shell prompt for instance.
* ToDo: raise the time, incrementaly, when no output is coming
* so the timer will kick in less and less.
*/
static void telnet_send_prematurely(struct k_timer *timer);
static K_TIMER_DEFINE(send_timer, telnet_send_prematurely, NULL);
/* For now we handle a unique telnet client connection */
static struct net_context *client_cnx;
static struct net_buf *out_buf;
static int (*orig_printk_hook)(int);
static struct k_fifo *avail_queue;
static struct k_fifo *input_queue;
#ifdef CONFIG_TELNET_CONSOLE_SUPPORT_COMMAND
static K_SEM_DEFINE(cmd_lock, 1, 1);
static struct telnet_simple_command telnet_cmd;
#endif /* CONFIG_TELNET_CONSOLE_SUPPORT_COMMAND */
extern void __printk_hook_install(int (*fn)(int));
extern void *__printk_get_hook(void);
static void telnet_rb_init(void)
{
int i;
telnet_rb.line_in = 0;
telnet_rb.line_out = 0;
for (i = 0; i < TELNET_LINES; i++) {
telnet_rb.l_bufs[i].len = 0;
}
}
static void telnet_end_client_connection(void)
{
__printk_hook_install(orig_printk_hook);
orig_printk_hook = NULL;
k_timer_stop(&send_timer);
net_context_put(client_cnx);
client_cnx = NULL;
if (out_buf) {
net_buf_unref(out_buf);
}
telnet_rb_init();
}
static int telnet_setup_out_buf(struct net_context *client)
{
out_buf = net_nbuf_get_tx(client);
if (!out_buf) {
/* Cannot happen atm, nbuf waits indefinitely */
return -ENOBUFS;
}
return 0;
}
static void telnet_rb_switch(void)
{
telnet_rb.line_in++;
if (telnet_rb.line_in == TELNET_LINES) {
telnet_rb.line_in = 0;
}
telnet_rb.l_bufs[telnet_rb.line_in].len = 0;
/* Unfortunately, we don't have enough line buffer,
* so we eat the next to be sent.
*/
if (telnet_rb.line_in == telnet_rb.line_out) {
telnet_rb.line_out++;
if (telnet_rb.line_out == TELNET_LINES) {
telnet_rb.line_out = 0;
}
}
k_timer_start(&send_timer, TELNET_TIMEOUT, TELNET_TIMEOUT);
k_sem_give(&send_lock);
}
static inline struct line_buf *telnet_rb_get_line_out(void)
{
uint16_t out = telnet_rb.line_out;
telnet_rb.line_out++;
if (telnet_rb.line_out == TELNET_LINES) {
telnet_rb.line_out = 0;
}
if (!telnet_rb.l_bufs[out].len) {
return NULL;
}
return &telnet_rb.l_bufs[out];
}
static inline struct line_buf *telnet_rb_get_line_in(void)
{
return &telnet_rb.l_bufs[telnet_rb.line_in];
}
/* The actual printk hook */
static int telnet_console_out(int c)
{
int key = irq_lock();
struct line_buf *lb = telnet_rb_get_line_in();
bool yield = false;
lb->buf[lb->len++] = (char)c;
if (c == '\n' || lb->len == TELNET_LINE_SIZE - 1) {
lb->buf[lb->len-1] = NVT_CR;
lb->buf[lb->len++] = NVT_LF;
telnet_rb_switch();
yield = true;
}
irq_unlock(key);
#ifdef CONFIG_TELNET_CONSOLE_DEBUG_DEEP
/* This is ugly, but if one wants to debug telnet, it
* will also output the character to original console
*/
orig_printk_hook(c);
#endif
if (yield) {
k_yield();
}
return c;
}
static void telnet_send_prematurely(struct k_timer *timer)
{
struct line_buf *lb = telnet_rb_get_line_in();
if (lb->len >= TELNET_THRESHOLD) {
telnet_rb_switch();
}
}
static void telnet_sent_cb(struct net_context *client,
int status, void *token, void *user_data)
{
if (status) {
telnet_end_client_connection();
SYS_LOG_ERR("Could not sent last buffer");
}
}
static inline bool telnet_send(void)
{
struct line_buf *lb = telnet_rb_get_line_out();
if (lb) {
net_nbuf_append(out_buf, lb->len, lb->buf);
/* We reinitialize the line buffer */
lb->len = 0;
if (net_context_send(out_buf, telnet_sent_cb,
K_NO_WAIT, NULL, NULL) ||
telnet_setup_out_buf(client_cnx)) {
return false;
}
}
return true;
}
#ifdef CONFIG_TELNET_CONSOLE_SUPPORT_COMMAND
static int telnet_console_out_nothing(int c)
{
return c;
}
static inline void telnet_command_send_reply(uint8_t *msg, uint16_t len)
{
net_nbuf_append(out_buf, len, msg);
net_context_send(out_buf, telnet_sent_cb,
K_NO_WAIT, NULL, NULL);
telnet_setup_out_buf(client_cnx);
}
static inline void telnet_reply_ay_command(void)
{
static const char alive[24] = "Zephyr at your service\r\n";
telnet_command_send_reply((uint8_t *)alive, 24);
}
static inline void telnet_reply_do_command(void)
{
switch (telnet_cmd.opt) {
case NVT_OPT_SUPR_GA:
telnet_cmd.op = NVT_CMD_WILL;
break;
default:
telnet_cmd.op = NVT_CMD_WONT;
break;
}
telnet_command_send_reply((uint8_t *)&telnet_cmd,
sizeof(struct telnet_simple_command));
}
static inline void telnet_reply_command(void)
{
if (k_sem_take(&cmd_lock, K_NO_WAIT)) {
return;
}
if (!telnet_cmd.iac) {
goto out;
}
switch (telnet_cmd.op) {
case NVT_CMD_AO:
/* OK, no output then */
__printk_hook_install(telnet_console_out_nothing);
telnet_rb_init();
break;
case NVT_CMD_AYT:
telnet_reply_ay_command();
break;
case NVT_CMD_DO:
telnet_reply_do_command();
break;
default:
SYS_LOG_DBG("Operation %u not handled",
telnet_cmd.op);
break;
}
telnet_cmd.iac = NVT_NUL;
telnet_cmd.op = NVT_NUL;
telnet_cmd.opt = NVT_NUL;
out:
k_sem_give(&cmd_lock);
}
#else
#define telnet_reply_command()
#endif /* CONFIG_TELNET_CONSOLE_SUPPORT_COMMAND */
static inline bool telnet_handle_command(struct net_buf *buf)
{
struct telnet_simple_command *cmd =
(struct telnet_simple_command *)net_nbuf_appdata(buf);
if (cmd->iac != NVT_CMD_IAC) {
return false;
}
#ifdef CONFIG_TELNET_CONSOLE_SUPPORT_COMMAND
cmd = (struct telnet_simple_command *)l_start;
SYS_LOG_DBG("Got a command %u/%u/%u", cmd->iac, cmd->op, cmd->opt);
if (!k_sem_take(&cmd_lock, K_NO_WAIT)) {
telnet_command_cpy(&telnet_cmd, cmd);
k_sem_give(&cmd_lock);
k_sem_give(&send_lock);
}
#endif /* CONFIG_TELNET_CONSOLE_SUPPORT_COMMAND */
return true;
}
static inline void telnet_handle_input(struct net_buf *buf)
{
struct console_input *input;
uint16_t len, offset, pos;
len = net_nbuf_appdatalen(buf);
if (len > CONSOLE_MAX_LINE_LEN || len < TELNET_MIN_MSG) {
return;
}
if (telnet_handle_command(buf)) {
return;
}
if (!avail_queue || !input_queue) {
return;
}
input = k_fifo_get(avail_queue, K_NO_WAIT);
if (!input) {
return;
}
offset = net_buf_frags_len(buf) - len;
net_nbuf_read(buf->frags, offset, &pos, len, input->line);
/* LF/CR will be removed if only the line is not NUL terminated */
if (input->line[len-1] != NVT_NUL) {
if (input->line[len-1] == NVT_LF) {
input->line[len-1] = NVT_NUL;
}
if (input->line[len-2] == NVT_CR) {
input->line[len-2] = NVT_NUL;
}
}
k_fifo_put(input_queue, input);
}
static void telnet_recv(struct net_context *client,
struct net_buf *buf,
int status,
void *user_data)
{
if (!buf || status) {
telnet_end_client_connection();
SYS_LOG_DBG("Telnet client dropped (AF_INET%s) status %d",
net_context_get_family(client) == AF_INET ?
"" : "6", status);
return;
}
telnet_handle_input(buf);
net_buf_unref(buf);
}
/* Telnet server loop, used to send buffered output in the RB */
static void telnet_run(void)
{
while (true) {
k_sem_take(&send_lock, K_FOREVER);
if (!telnet_send()) {
telnet_end_client_connection();
}
telnet_reply_command();
}
}
static void telnet_accept(struct net_context *client,
struct sockaddr *addr,
socklen_t addrlen,
int error,
void *user_data)
{
if (error) {
SYS_LOG_ERR("Error %d", error);
goto error;
}
if (client_cnx) {
SYS_LOG_WRN("A telnet client is already in.");
goto error;
}
if (net_context_recv(client, telnet_recv, 0, NULL)) {
SYS_LOG_ERR("Unable to setup reception (family %u)",
net_context_get_family(client));
goto error;
}
if (telnet_setup_out_buf(client)) {
goto error;
}
SYS_LOG_DBG("Telnet client connected (family AF_INET%s)",
net_context_get_family(client) == AF_INET ? "" : "6");
orig_printk_hook = __printk_get_hook();
__printk_hook_install(telnet_console_out);
client_cnx = client;
k_timer_start(&send_timer, TELNET_TIMEOUT, TELNET_TIMEOUT);
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)) {
SYS_LOG_ERR("No context available");
goto error;
}
if (net_context_bind(*ctx, addr, addrlen)) {
SYS_LOG_ERR("Cannot bind on family AF_INET%s",
family == AF_INET ? "" : "6");
goto error;
}
if (net_context_listen(*ctx, 0)) {
SYS_LOG_ERR("Cannot listen on");
goto error;
}
if (net_context_accept(*ctx, telnet_accept, 0, NULL)) {
SYS_LOG_ERR("Cannot accept");
goto error;
}
SYS_LOG_DBG("Telnet console enabled on AF_INET%s",
family == AF_INET ? "" : "6");
return;
error:
SYS_LOG_ERR("Unable to start telnet on AF_INET%s",
family == AF_INET ? "" : "6");
if (*ctx) {
net_context_put(*ctx);
*ctx = NULL;
}
}
void telnet_register_input(struct k_fifo *avail, struct k_fifo *lines,
uint8_t (*completion)(char *str, uint8_t len))
{
ARG_UNUSED(completion);
avail_queue = avail;
input_queue = lines;
}
static int telnet_console_init(struct device *arg)
{
#ifdef 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;
#endif
#ifdef 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;
#endif
#ifdef CONFIG_NET_IPV4
telnet_setup_server(&ctx4, AF_INET,
(struct sockaddr *)&any_addr4,
sizeof(any_addr4));
#endif
#ifdef CONFIG_NET_IPV6
telnet_setup_server(&ctx6, AF_INET6,
(struct sockaddr *)&any_addr6,
sizeof(any_addr6));
#endif
k_thread_spawn(&telnet_stack[0],
TELNET_STACK_SIZE,
(k_thread_entry_t)telnet_run,
NULL, NULL, NULL,
K_PRIO_COOP(TELNET_PRIORITY), 0, K_MSEC(10));
SYS_LOG_INF("Telnet console initialized");
return 0;
}
/* Telnet is initialized as an application directly, as it requires
* the whole network stack to be ready.
*/
SYS_INIT(telnet_console_init, APPLICATION, CONFIG_TELNET_CONSOLE_INIT_PRIORITY);