From 658a08a4cb395b57a230931516f3fda5f5eb9c5f Mon Sep 17 00:00:00 2001 From: Robert Lubos Date: Thu, 28 Mar 2019 11:54:14 +0100 Subject: [PATCH] shell: Add TELNET backend Add TELNET backed for shell module. The TELNET implementation is based on the telnet_console driver. Signed-off-by: Robert Lubos --- include/shell/shell_telnet.h | 75 ++++ subsys/shell/CMakeLists.txt | 5 + subsys/shell/Kconfig.backends | 88 +++++ subsys/shell/shell_telnet.c | 498 +++++++++++++++++++++++++++ subsys/shell/shell_telnet_protocol.h | 125 +++++++ 5 files changed, 791 insertions(+) create mode 100644 include/shell/shell_telnet.h create mode 100644 subsys/shell/shell_telnet.c create mode 100644 subsys/shell/shell_telnet_protocol.h diff --git a/include/shell/shell_telnet.h b/include/shell/shell_telnet.h new file mode 100644 index 00000000000..da7ff5fdf84 --- /dev/null +++ b/include/shell/shell_telnet.h @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef SHELL_TELNET_H__ +#define SHELL_TELNET_H__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +extern const struct shell_transport_api shell_telnet_transport_api; + +/** Line buffer structure. */ +struct shell_telnet_line_buf { + /** Line buffer. */ + char buf[CONFIG_SHELL_TELNET_LINE_BUF_SIZE]; + + /** Current line length. */ + u16_t len; +}; + +/** TELNET-based shell transport. */ +struct shell_telnet { + /** Handler function registered by shell. */ + shell_transport_handler_t shell_handler; + + /** Context registered by shell. */ + void *shell_context; + + /** Buffer for outgoing line. */ + struct shell_telnet_line_buf line_out; + + /** Network context of TELNET client. */ + struct net_context *client_ctx; + + /** RX packet FIFO. */ + struct k_fifo rx_fifo; + + /** The timer is used to send non-lf terminated output that has + * been around for "too long". This will prove to be useful + * to send the shell prompt for instance. + */ + struct k_timer send_timer; + + /** If set, no output is sent to the TELNET client. */ + bool output_lock; +}; + +#define SHELL_TELNET_DEFINE(_name) \ + static struct shell_telnet _name##_shell_telnet; \ + struct shell_transport _name = { \ + .api = &shell_telnet_transport_api, \ + .ctx = (struct shell_telnet *)&_name##_shell_telnet \ + } + +/** + * @brief This function provides pointer to shell telnet backend instance. + * + * Function returns pointer to the shell telnet instance. This instance can be + * next used with shell_execute_cmd function in order to test commands behavior. + * + * @returns Pointer to the shell instance. + */ +const struct shell *shell_backend_telnet_get_ptr(void); + +#ifdef __cplusplus +} +#endif + +#endif /* SHELL_TELNET_H__ */ diff --git a/subsys/shell/CMakeLists.txt b/subsys/shell/CMakeLists.txt index 3fe30190c60..3257a64bf25 100644 --- a/subsys/shell/CMakeLists.txt +++ b/subsys/shell/CMakeLists.txt @@ -23,6 +23,11 @@ zephyr_sources_ifdef( shell_rtt.c ) +zephyr_sources_ifdef( + CONFIG_SHELL_BACKEND_TELNET + shell_telnet.c +) + zephyr_sources_ifdef( CONFIG_SHELL_HELP shell_help.c diff --git a/subsys/shell/Kconfig.backends b/subsys/shell/Kconfig.backends index 978c875c1ee..90358229223 100644 --- a/subsys/shell/Kconfig.backends +++ b/subsys/shell/Kconfig.backends @@ -167,6 +167,94 @@ source "subsys/logging/Kconfig.template.log_config" endif #SHELL_BACKEND_RTT +config SHELL_BACKEND_TELNET + bool "Enable TELNET backend." + depends on NET_TCP + depends on NET_IPV4 || NET_IPV6 + help + Enable TELNET backend. + +if SHELL_BACKEND_TELNET + +config SHELL_TELNET_PORT + int "Telnet port number" + default 23 + help + This option is used to configure on which port telnet is going + to be bound. + +config SHELL_TELNET_LINE_BUF_SIZE + int "Telnet line buffer size" + default 80 + help + This option can be used to modify the size of the buffer storing + shell output line, prior to sending it through the network. + Of course an output line can be longer than such size, it just + means sending it will start as soon as it reaches this size. + It really depends on what type of output is expected. + A lot of short lines: better reduce this value. On the contrary, + raise it. + +config SHELL_TELNET_SEND_TIMEOUT + int "Telnet line send timeout" + default 100 + help + This option can be used to modify the duration of the timer that kick + in when a line buffer is not empty but did not yet meet the line feed. + +config SHELL_TELNET_SUPPORT_COMMAND + bool "Add support for telnet commands (IAC) [Experimental]" + help + Current support is so limited it's not interesting to enable it. + However, if proven to be needed at some point, it will be possible + to extend such support. + +module = SHELL_TELNET +default-timeout = 100 +source "subsys/shell/Kconfig.template.shell_log_queue_timeout" + +default-size = 10 +source "subsys/shell/Kconfig.template.shell_log_queue_size" + +choice + prompt "Initial log level limit" + default SHELL_TELNET_INIT_LOG_LEVEL_DEFAULT + +config SHELL_TELNET_INIT_LOG_LEVEL_DEFAULT + bool "System limit (LOG_MAX_LEVEL)" + +config SHELL_TELNET_INIT_LOG_LEVEL_DBG + bool "Debug" + +config SHELL_TELNET_INIT_LOG_LEVEL_INF + bool "Info" + +config SHELL_TELNET_INIT_LOG_LEVEL_WRN + bool "Warning" + +config SHELL_TELNET_INIT_LOG_LEVEL_ERR + bool "Error" + +config SHELL_TELNET_INIT_LOG_LEVEL_NONE + bool "None" + +endchoice + +config SHELL_TELNET_INIT_LOG_LEVEL + int + default 0 if SHELL_TELNET_INIT_LOG_LEVEL_NONE + default 1 if SHELL_TELNET_INIT_LOG_LEVEL_ERR + default 2 if SHELL_TELNET_INIT_LOG_LEVEL_WRN + default 3 if SHELL_TELNET_INIT_LOG_LEVEL_INF + default 4 if SHELL_TELNET_INIT_LOG_LEVEL_DBG + default 5 if SHELL_TELNET_INIT_LOG_LEVEL_DEFAULT + +module = SHELL_TELNET +module-str = TELNET shell backend +source "subsys/logging/Kconfig.template.log_config" + +endif # SHELL_TELNET_BACKEND + config SHELL_BACKEND_DUMMY bool "Enable dummy backend." help diff --git a/subsys/shell/shell_telnet.c b/subsys/shell/shell_telnet.c new file mode 100644 index 00000000000..2503ec52b79 --- /dev/null +++ b/subsys/shell/shell_telnet.c @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2017 Intel Corporation + * Copyright (c) 2019 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include + +#include "shell_telnet_protocol.h" + +SHELL_TELNET_DEFINE(shell_transport_telnet); +SHELL_DEFINE(shell_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 K_MSEC(CONFIG_SHELL_TELNET_SEND_TIMEOUT) + +#define TELNET_MIN_MSG 2 + +/* Basic TELNET implmentation. */ + +static void telnet_end_client_connection(void) +{ + struct net_pkt *pkt; + + net_context_put(sh_telnet->client_ctx); + sh_telnet->client_ctx = NULL; + sh_telnet->output_lock = false; + + k_timer_stop(&sh_telnet->send_timer); + + /* 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(u8_t *msg, u16_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((u8_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((u8_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_timer_stop(&sh_telnet->send_timer); + 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_timer *timer) +{ + (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, u16_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_MSG) { + LOG_DBG("Packet smaller than minimum length"); + goto unref; + } + + 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, 0, NULL)) { + LOG_ERR("Unable to setup reception (family %u)", + net_context_get_family(client)); + goto error; + } + + 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) { + 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_timer_init(&sh_telnet->send_timer, telnet_send_prematurely, NULL); + + 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; + u32_t timeout; + + 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_timer_remaining_get(&sh_telnet->send_timer); + k_timer_stop(&sh_telnet->send_timer); + + 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, (u8_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 = (timeout == 0) ? TELNET_TIMEOUT : timeout; + + k_timer_start(&sh_telnet->send_timer, timeout, 0); + } + + 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; + net_pkt_read(pkt, data, read_len); + + 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(struct device *arg) +{ + ARG_UNUSED(arg); + + bool log_backend = CONFIG_SHELL_TELNET_INIT_LOG_LEVEL > 0; + u32_t level = (CONFIG_SHELL_TELNET_INIT_LOG_LEVEL > LOG_LEVEL_DBG) ? + CONFIG_LOG_MAX_LEVEL : CONFIG_SHELL_TELNET_INIT_LOG_LEVEL; + + shell_init(&shell_telnet, NULL, true, log_backend, level); + + return 0; +} + +SYS_INIT(enable_shell_telnet, APPLICATION, 0); + +const struct shell *shell_backend_telnet_get_ptr(void) +{ + return &shell_telnet; +} diff --git a/subsys/shell/shell_telnet_protocol.h b/subsys/shell/shell_telnet_protocol.h new file mode 100644 index 00000000000..a77ca29d9da --- /dev/null +++ b/subsys/shell/shell_telnet_protocol.h @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2017 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Telnet console protocol specific defines + * + * + * This defines the Telnet codes, all prefixed as NVT_ + * (NVT: Network Virtual Terminal, see rfc854) + */ + +#ifndef SHELL_TELNET_PROTOCOL_H__ +#define SHELL_TELNET_PROTOCOL_H__ + +/** Printer/Keyboard codes */ + +/* Mandatory ones */ +#define NVT_NUL 0 +#define NVT_LF 10 +#define NVT_CR 13 + +/* Optional ones */ +#define NVT_BEL 7 +#define NVT_BS 8 +#define NVT_HT 9 +#define NVT_VT 11 +#define NVT_FF 12 + +/* Telnet commands */ +#define NVT_CMD_SE 240 +#define NVT_CMD_NOP 241 +#define NVT_CMD_DM 242 +#define NVT_CMD_BRK 243 +#define NVT_CMD_IP 244 +#define NVT_CMD_AO 245 +#define NVT_CMD_AYT 246 +#define NVT_CMD_EC 247 +#define NVT_CMD_EL 248 +#define NVT_CMD_GA 249 +#define NVT_CMD_SB 250 +#define NVT_CMD_WILL 251 +#define NVT_CMD_WONT 252 +#define NVT_CMD_DO 253 +#define NVT_CMD_DONT 254 +#define NVT_CMD_IAC 255 + +/* Telnet options */ +#define NVT_OPT_TX_BIN 0 +#define NVT_OPT_ECHO 1 +#define NVT_OPT_RECONNECT 2 +#define NVT_OPT_SUPR_GA 3 +#define NVT_OPT_MSG_SZ_NEG 4 +#define NVT_OPT_STATUS 5 +#define NVT_OPT_TIMING_MARK 6 +#define NVT_OPT_REMOTE_CTRL_TRANS_ECHO 7 +#define NVT_OPT_OUT_LINE_WIDTH 8 +#define NVT_OPT_OUT_PAGE_SZ 9 +#define NVT_OPT_NEG_CR 10 +#define NVT_OPT_NEG_HT 11 +#define NVT_OPT_NAOHTD 12 +#define NVT_OPT_NEG_OUT_FF 13 +#define NVT_OPT_NEG_VT 14 +#define NVT_OPT_NEG_OUT_VT 15 +#define NVT_OPT_NET_OUT_LF 16 +#define NVT_OPT_EXT_ASCII 17 +#define NVT_OPT_LOGOUT 18 +#define NVT_OPT_BYTE_MACRO 19 +#define NVT_OPT_DATA_ENTRY 20 +#define NVT_OPT_SUPDUP 21 +#define NVT_OPT_SUPDUP_OUT 22 +#define NVT_OPT_SEND_LOC 23 +#define NVT_OPT_TERM_TYPE 24 +#define NVT_OPT_EOR 25 +#define NVT_OPT_TACACS_UID 26 +#define NVT_OPT_OUT_MARK 27 +#define NVT_OPT_TTYLOC 28 +#define NVT_OPT_3270 29 +#define NVT_OPT_X_3_PAD 30 +#define NVT_OPT_NAWS 31 +#define NVT_OPT_TERM_SPEED 32 +#define NVT_OPT_REMOTE_FC 33 +#define NVT_OPT_LINEMODE 34 +#define NVT_OPT_X_LOC 35 +#define NVT_OPT_ENV 36 +#define NVT_OPT_AUTH 37 +#define NVT_OPT_ENCRYPT_OPT 38 +#define NVT_OPT_NEW_ENV 39 +#define NVT_OPT_TN3270E 40 +#define NVT_OPT_XAUTH 41 +#define NVT_OPT_CHARSET 42 +#define NVT_OPT_RSP 43 +#define NVT_OPT_COM_PORT_CTRL 44 +#define NVT_OPT_SUPR_LOCAL_ECHO 45 +#define NVT_OPT_START_TLS 46 +#define NVT_OPT_KERMIT 47 +#define NVT_OPT_SEND_URL 48 +#define NVT_OPT_FORWARD_X 49 +#define NVT_OPT_PRAGMA_LOGON 138 +#define NVT_OPT_SSPI_LOGON 139 +#define NVT_OPT_PRAGMA_HB 140 +#define NVT_OPT_EXT_OPT_LIST 255 + +/** Describes a telnet command */ +struct telnet_simple_command { + /** Mandatory IAC code */ + u8_t iac; + /** Type of operation (see Telnet commands above) */ + u8_t op; + /** Option code */ + u8_t opt; +}; + +static inline void telnet_command_cpy(struct telnet_simple_command *dst, + struct telnet_simple_command *src) +{ + dst->iac = src->iac; + dst->op = src->op; + dst->opt = src->opt; +} + +#endif /* SHELL_TELNET_PROTOCOL_H__ */