diff --git a/drivers/console/CMakeLists.txt b/drivers/console/CMakeLists.txt index 2f9cc373d73eb..59a764468ce07 100644 --- a/drivers/console/CMakeLists.txt +++ b/drivers/console/CMakeLists.txt @@ -8,3 +8,4 @@ zephyr_sources_if_kconfig(uart_pipe.c) zephyr_sources_if_kconfig(telnet_console.c) zephyr_sources_if_kconfig(xtensa_sim_console.c) zephyr_sources_if_kconfig(native_posix_console.c) +zephyr_sources_if_kconfig(websocket_console.c) diff --git a/drivers/console/Kconfig b/drivers/console/Kconfig index db1aa6c6c4da7..7e6f7d3a998eb 100644 --- a/drivers/console/Kconfig +++ b/drivers/console/Kconfig @@ -312,4 +312,5 @@ config NATIVE_POSIX_CONSOLE_INIT_PRIORITY Device driver initialization priority. source "drivers/console/Kconfig.telnet" +source "drivers/console/Kconfig.ws" endif diff --git a/drivers/console/Kconfig.ws b/drivers/console/Kconfig.ws new file mode 100644 index 0000000000000..67dad14e595da --- /dev/null +++ b/drivers/console/Kconfig.ws @@ -0,0 +1,112 @@ +# Kconfig - console driver configuration options + +# +# Copyright (c) 2017 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 +# + +menuconfig WEBSOCKET_CONSOLE + bool "Enable websocket console service" + default n + select NETWORKING + select NET_TCP + select HTTP_PARSER + select HTTP_SERVER + select WEBSOCKET + help + This option enables console over a websocket link. Currently, + it is basically just a redirection of the Zephyr console through + websocket. It nicely works along with another console driver (like + uart), twist being that it will take over the output only if a + successful connection to its HTTP service is done. + +if WEBSOCKET_CONSOLE + +config WEBSOCKET_CONSOLE_LINE_BUF_SIZE + int "WS console line buffer size" + default 128 + help + This option can be used to modify the size of the buffer storing + console 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. + If there is a lot of short lines, then lower this value. If there + are longer lines, then raise this value. + +config WEBSOCKET_CONSOLE_LINE_BUF_NUMBERS + int "WS console line buffers" + default 4 + help + This option can be used to modify the amount of line buffers the + driver can use. It really depends on how much output is meant to be + sent, depending on the system load etc. You can play on both + WEBSOCKET_CONSOLE_LINE_BUF_SIZE and this current option to get the + best possible buffer settings you need. + +config WEBSOCKET_CONSOLE_SEND_TIMEOUT + int "WS console 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 WEBSOCKET_CONSOLE_SEND_THRESHOLD + int "WS console line send threshold" + default 5 + help + This option can be used to modify the minimal amount of a line buffer + that can be sent by the WS server when nothing has happened for + a little while (see WEBSOCKET_CONSOLE_SEND_TIMEOUT) and when the line + buffer did not meet the line feed yet. + +config WEBSOCKET_CONSOLE_STACK_SIZE + int "WS console inner thread stack size" + default 1500 + help + This option helps to fine-tune WS console inner thread stack size. + +config WEBSOCKET_CONSOLE_PRIO + int "WS console inner thread priority" + default 7 + help + This option helps to fine-tune WS console inner thread priority. + +config SYS_LOG_WEBSOCKET_CONSOLE_LEVEL + int "WS console log level" + default 0 + depends on SYS_LOG + help + Sets log level for websocket console (for WS console dev only) + + Levels are: + + - 0 OFF, do not write + + - 1 ERROR, only write SYS_LOG_ERR + + - 2 WARNING, write SYS_LOG_WRN in addition to previous level + + - 3 INFO, write SYS_LOG_INF in addition to previous levels + + - 4 DEBUG, write SYS_LOG_DBG in addition to previous levels + +config WEBSOCKET_CONSOLE_DEBUG_DEEP + bool "Forward output to original console handler" + depends on UART_CONSOLE + default n + help + For WS console developers only, this will forward each output to + original console handler. So if by chance WS console seems silent, + at least things will be printed to original handler, usually + UART console. + +config WEBSOCKET_CONSOLE_INIT_PRIORITY + int "WS console init priority" + default 99 + help + WS console driver initialization priority. Note that WS works + on application level. Usually, you won't have to tweak this. + +endif diff --git a/drivers/console/websocket_console.c b/drivers/console/websocket_console.c new file mode 100644 index 0000000000000..f0b628e7636ec --- /dev/null +++ b/drivers/console/websocket_console.c @@ -0,0 +1,339 @@ +/* + * Copyright (c) 2017 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Websocket console + * + * + * Websocket console driver. The console is provided over + * a websocket connection. + */ + +#define SYS_LOG_LEVEL CONFIG_SYS_LOG_WEBSOCKET_CONSOLE_LEVEL +#define SYS_LOG_DOMAIN "ws/console" +#include + +#include +#include +#include + +#include +#include +#include +#include + +#define NVT_NUL 0 +#define NVT_LF 10 +#define NVT_CR 13 + +#define WS_CONSOLE_STACK_SIZE CONFIG_WEBSOCKET_CONSOLE_STACK_SIZE +#define WS_CONSOLE_PRIORITY CONFIG_WEBSOCKET_CONSOLE_PRIO +#define WS_CONSOLE_TIMEOUT K_MSEC(CONFIG_WEBSOCKET_CONSOLE_SEND_TIMEOUT) +#define WS_CONSOLE_LINES CONFIG_WEBSOCKET_CONSOLE_LINE_BUF_NUMBERS +#define WS_CONSOLE_LINE_SIZE CONFIG_WEBSOCKET_CONSOLE_LINE_BUF_SIZE +#define WS_CONSOLE_TIMEOUT K_MSEC(CONFIG_WEBSOCKET_CONSOLE_SEND_TIMEOUT) +#define WS_CONSOLE_THRESHOLD CONFIG_WEBSOCKET_CONSOLE_SEND_THRESHOLD + +#define WS_CONSOLE_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_WEBSOCKET_CONSOLE_LINE_BUF_NUMBERS if possible. + */ +struct line_buf { + char buf[WS_CONSOLE_LINE_SIZE]; + u16_t len; +}; + +struct line_buf_rb { + struct line_buf l_bufs[WS_CONSOLE_LINES]; + u16_t line_in; + u16_t line_out; +}; + +static struct line_buf_rb ws_rb; + +NET_STACK_DEFINE(WS_CONSOLE, ws_console_stack, + WS_CONSOLE_STACK_SIZE, WS_CONSOLE_STACK_SIZE); +static struct k_thread ws_thread_data; +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 ws_send_prematurely(struct k_timer *timer); +static K_TIMER_DEFINE(send_timer, ws_send_prematurely, NULL); +static int (*orig_printk_hook)(int); + +static struct k_fifo *avail_queue; +static struct k_fifo *input_queue; + +/* Websocket context that this console is related to */ +static struct http_ctx *ws_console; + +extern void __printk_hook_install(int (*fn)(int)); +extern void *__printk_get_hook(void); + +void ws_register_input(struct k_fifo *avail, struct k_fifo *lines, + u8_t (*completion)(char *str, u8_t len)) +{ + ARG_UNUSED(completion); + + avail_queue = avail; + input_queue = lines; +} + +static void ws_rb_init(void) +{ + int i; + + ws_rb.line_in = 0; + ws_rb.line_out = 0; + + for (i = 0; i < WS_CONSOLE_LINES; i++) { + ws_rb.l_bufs[i].len = 0; + } +} + +static void ws_end_client_connection(struct http_ctx *console) +{ + __printk_hook_install(orig_printk_hook); + orig_printk_hook = NULL; + + k_timer_stop(&send_timer); + + ws_send_msg(console, NULL, 0, WS_OPCODE_CLOSE, false, true, + NULL, NULL); + + ws_rb_init(); +} + +static void ws_rb_switch(void) +{ + ws_rb.line_in++; + + if (ws_rb.line_in == WS_CONSOLE_LINES) { + ws_rb.line_in = 0; + } + + ws_rb.l_bufs[ws_rb.line_in].len = 0; + + /* Unfortunately, we don't have enough line buffer, + * so we eat the next to be sent. + */ + if (ws_rb.line_in == ws_rb.line_out) { + ws_rb.line_out++; + if (ws_rb.line_out == WS_CONSOLE_LINES) { + ws_rb.line_out = 0; + } + } + + k_timer_start(&send_timer, WS_CONSOLE_TIMEOUT, WS_CONSOLE_TIMEOUT); + k_sem_give(&send_lock); +} + +static inline struct line_buf *ws_rb_get_line_out(void) +{ + u16_t out = ws_rb.line_out; + + ws_rb.line_out++; + if (ws_rb.line_out == WS_CONSOLE_LINES) { + ws_rb.line_out = 0; + } + + if (!ws_rb.l_bufs[out].len) { + return NULL; + } + + return &ws_rb.l_bufs[out]; +} + +static inline struct line_buf *ws_rb_get_line_in(void) +{ + return &ws_rb.l_bufs[ws_rb.line_in]; +} + +/* The actual printk hook */ +static int ws_console_out(int c) +{ + int key = irq_lock(); + struct line_buf *lb = ws_rb_get_line_in(); + bool yield = false; + + lb->buf[lb->len++] = (char)c; + + if (c == '\n' || lb->len == WS_CONSOLE_LINE_SIZE - 1) { + lb->buf[lb->len-1] = NVT_CR; + lb->buf[lb->len++] = NVT_LF; + ws_rb_switch(); + yield = true; + } + + irq_unlock(key); + +#ifdef CONFIG_WEBSOCKET_CONSOLE_DEBUG_DEEP + /* This is ugly, but if one wants to debug websocket console, it + * will also output the character to original console + */ + orig_printk_hook(c); +#endif + + if (yield) { + k_yield(); + } + + return c; +} + +static void ws_send_prematurely(struct k_timer *timer) +{ + struct line_buf *lb = ws_rb_get_line_in(); + + if (lb->len >= WS_CONSOLE_THRESHOLD) { + ws_rb_switch(); + } +} + +static inline void ws_handle_input(struct net_pkt *pkt) +{ + struct console_input *input; + u16_t len, offset, pos; + + len = net_pkt_appdatalen(pkt); + if (len > CONSOLE_MAX_LINE_LEN || len < WS_CONSOLE_MIN_MSG) { + return; + } + + if (!avail_queue || !input_queue) { + return; + } + + input = k_fifo_get(avail_queue, K_NO_WAIT); + if (!input) { + return; + } + + offset = net_pkt_get_len(pkt) - len; + net_frag_read(pkt->frags, offset, &pos, len, (u8_t *)input->line); + + /* The data from websocket does not contain \n or NUL, so insert + * it here. + */ + input->line[len] = NVT_NUL; + + /* 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); +} + +/* The data is coming from outside system and going into zephyr */ +int ws_console_recv(struct http_ctx *ctx, struct net_pkt *pkt) +{ + if (ctx != ws_console) { + return -ENOENT; + } + + ws_handle_input(pkt); + + net_pkt_unref(pkt); + + return 0; +} + +/* This is for transferring data from zephyr to outside system */ +static bool ws_console_send(struct http_ctx *console) +{ + struct line_buf *lb = ws_rb_get_line_out(); + + if (lb) { + (void)ws_send_msg(console, (u8_t *)lb->buf, lb->len, + WS_OPCODE_DATA_TEXT, false, true, + NULL, NULL); + + /* We reinitialize the line buffer */ + lb->len = 0; + } + + return true; +} + +/* WS console loop, used to send buffered output in the RB */ +static void ws_console_run(void) +{ + while (true) { + k_sem_take(&send_lock, K_FOREVER); + + if (!ws_console_send(ws_console)) { + ws_end_client_connection(ws_console); + } + } +} + +int ws_console_enable(struct http_ctx *ctx) +{ + orig_printk_hook = __printk_get_hook(); + __printk_hook_install(ws_console_out); + + k_timer_start(&send_timer, WS_CONSOLE_TIMEOUT, WS_CONSOLE_TIMEOUT); + + ws_console = ctx; + + return 0; +} + +int ws_console_disable(struct http_ctx *ctx) +{ + if (!ws_console) { + return 0; + } + + if (ws_console != ctx) { + return -ENOENT; + } + + ws_end_client_connection(ws_console); + + ws_console = NULL; + + return 0; +} + +static int ws_console_init(struct device *arg) +{ + k_thread_create(&ws_thread_data, ws_console_stack, + K_THREAD_STACK_SIZEOF(ws_console_stack), + (k_thread_entry_t)ws_console_run, + NULL, NULL, NULL, + K_PRIO_COOP(WS_CONSOLE_PRIORITY), 0, K_MSEC(10)); + + SYS_LOG_INF("Websocket console initialized"); + + return 0; +} + +/* Websocket console is initialized as an application directly, as it requires + * the whole network stack to be ready. + */ +SYS_INIT(ws_console_init, APPLICATION, CONFIG_WEBSOCKET_CONSOLE_INIT_PRIORITY); diff --git a/include/drivers/console/websocket_console.h b/include/drivers/console/websocket_console.h new file mode 100644 index 0000000000000..2ce36ac5e98b8 --- /dev/null +++ b/include/drivers/console/websocket_console.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2017 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __WS_CONSOLE_H__ +#define __WS_CONSOLE_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/** @brief Register websocket input processing + * + * Input processing is started when string is received in WS server. + * Carriage return is translated to NULL making string always NULL + * terminated. Application before calling register function need to + * initialize two fifo queues mentioned below. + * + * @param avail k_fifo queue keeping available input slots + * @param lines k_fifo queue of entered lines which to be processed + * in the application code. + * @param completion callback for tab completion of entered commands + * + * @return N/A + */ +void ws_register_input(struct k_fifo *avail, struct k_fifo *lines, + u8_t (*completion)(char *str, u8_t len)); + +#ifdef __cplusplus +} +#endif + +#endif /* __WS_CONSOLE_H__ */ diff --git a/include/net/http.h b/include/net/http.h index 373caf75cd7ed..13d451a13f1c5 100644 --- a/include/net/http.h +++ b/include/net/http.h @@ -435,6 +435,27 @@ struct http_ctx { u16_t url_len; } http; +#if defined(CONFIG_WEBSOCKET) + struct { + /** Pending data that is not yet ready for processing */ + struct net_pkt *pending; + + /** Amount of data that needs to be read still */ + u32_t data_waiting; + + /** Websocket connection masking value */ + u32_t masking_value; + + /** How many bytes we have read */ + u32_t data_read; + + /** Message type flag. Value is one of WS_FLAG_XXX flag values + * defined in weboscket.h + */ + u32_t msg_type_flag; + } websocket; +#endif /* CONFIG_WEBSOCKET */ + #if defined(CONFIG_NET_DEBUG_HTTP_CONN) sys_snode_t node; #endif /* CONFIG_HTTP_DEBUG_HTTP_CONN */ diff --git a/include/net/websocket.h b/include/net/websocket.h new file mode 100644 index 0000000000000..880ac178c1066 --- /dev/null +++ b/include/net/websocket.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2017 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __WEBSOCKET_H__ +#define __WEBSOCKET_H__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Websocket library + * @defgroup websocket Websocket Library + * @{ + */ + +/** Values for flag variable in HTTP receive callback */ +#define WS_FLAG_FINAL 0x00000001 +#define WS_FLAG_TEXT 0x00000002 +#define WS_FLAG_BINARY 0x00000004 +#define WS_FLAG_CLOSE 0x00000008 +#define WS_FLAG_PING 0x00000010 +#define WS_FLAG_PONG 0x00000011 + +enum ws_opcode { + WS_OPCODE_CONTINUE = 0x00, + WS_OPCODE_DATA_TEXT = 0x01, + WS_OPCODE_DATA_BINARY = 0x02, + WS_OPCODE_CLOSE = 0x08, + WS_OPCODE_PING = 0x09, + WS_OPCODE_PONG = 0x0A, +}; + +/** + * @brief Send websocket msg to peer. + * + * @details The function will automatically add websocket header to the + * message. + * + * @param ctx Websocket context. + * @param payload Websocket data to send. + * @param payload_len Length of the data to be sent. + * @param opcode Operation code (text, binary, ping, pong, close) + * @param mask Mask the data, see RFC 6455 for details + * @param final Is this final message for this message send. If final == false, + * then the first message must have valid opcode and subsequent messages must + * have opcode WS_OPCODE_CONTINUE. If final == true and this is the only + * message, then opcode should have proper opcode (text or binary) set. + * @param dst Remote socket address + * @param user_send_data User specific data to this connection. This is passed + * as a parameter to sent cb after the packet has been sent. + * + * @return 0 if ok, <0 if error. + */ +int ws_send_msg(struct http_ctx *ctx, u8_t *payload, size_t payload_len, + enum ws_opcode opcode, bool mask, bool final, + const struct sockaddr *dst, + void *user_send_data); + +/** + * @brief Send message to client. + * + * @details The function will automatically add websocket header to the + * message. + * + * @param ctx Websocket context. + * @param payload Websocket data to send. + * @param payload_len Length of the data to be sent. + * @param opcode Operation code (text, binary, ping ,pong ,close) + * @param final Is this final message for this data send + * @param dst Remote socket address + * @param user_send_data User specific data to this connection. This is passed + * as a parameter to sent cb after the packet has been sent. + * + * @return 0 if ok, <0 if error. + */ +static inline int ws_send_msg_to_client(struct http_ctx *ctx, + u8_t *payload, + size_t payload_len, + enum ws_opcode opcode, + bool final, + const struct sockaddr *dst, + void *user_send_data) +{ + return ws_send_msg(ctx, payload, payload_len, opcode, false, final, + dst, user_send_data); +} + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* __WS_H__ */ diff --git a/include/net/websocket_console.h b/include/net/websocket_console.h new file mode 100644 index 0000000000000..4a626d1cda953 --- /dev/null +++ b/include/net/websocket_console.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2017 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __WEBSOCKET_CONSOLE_H__ +#define __WEBSOCKET_CONSOLE_H__ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Websocket console library + * @defgroup websocket_console Websocket Console Library + * @{ + */ + +/** + * @brief Enable websocket console. + * + * @details The console can be sent over websocket to browser. + * + * @param ctx HTTP context + * + * @return 0 if ok, <0 if error + */ +int ws_console_enable(struct http_ctx *ctx); + +/** + * @brief Disable websocket console. + * + * @param ctx HTTP context + * + * @return 0 if ok, <0 if error + */ +int ws_console_disable(struct http_ctx *ctx); + +/** + * @brief Receive data from outside system and feed it into Zephyr. + * + * @param ctx HTTP context + * @param pkt Network packet containing the received data. + * + * @return 0 if ok, <0 if error + */ +int ws_console_recv(struct http_ctx *ctx, struct net_pkt *pkt); + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* __WEBSOCKET_CONSOLE_H__ */ diff --git a/samples/net/ws_console/CMakeLists.txt b/samples/net/ws_console/CMakeLists.txt new file mode 100644 index 0000000000000..7427031bcc3ff --- /dev/null +++ b/samples/net/ws_console/CMakeLists.txt @@ -0,0 +1,28 @@ +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(NONE) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) + +include($ENV{ZEPHYR_BASE}/samples/net/common/common.cmake) + +set(gen_dir ${ZEPHYR_BINARY_DIR}/include/generated/) + +# List of files that are used to generate .h file that can be included +# into .c file. +foreach(inc_file + echo-apps-cert.der + echo-apps-key.der + ) + generate_inc_file_for_target( + app + src/${inc_file} + ${gen_dir}/${inc_file}.inc + ) +endforeach() + +generate_inc_file_for_target(app src/index.html ${gen_dir}/index.html.gz.inc --gzip) +generate_inc_file_for_target(app src/style.css ${gen_dir}/style.css.gz.inc --gzip) +generate_inc_file_for_target(app src/favicon.ico ${gen_dir}/favicon.ico.gz.inc --gzip) + +target_link_libraries_ifdef(CONFIG_MBEDTLS app mbedTLS) diff --git a/samples/net/ws_console/README.rst b/samples/net/ws_console/README.rst new file mode 100644 index 0000000000000..043827cbe9cb6 --- /dev/null +++ b/samples/net/ws_console/README.rst @@ -0,0 +1,40 @@ +.. _websocket-console-sample: + +Websocket Console +################# + +Overview +******** + +The websocket-console sample application for Zephyr implements a console +over a websocket. The websocket-console sample application listens for incoming +IPv4 or IPv6 HTTP(S) requests and provides Zephyr console to the browser over +a websocket. + +The source code for this sample application can be found at: +:file:`samples/net/ws_console`. + +Requirements +************ + +- :ref:`networking_with_qemu` + +Building and Running +******************** + +There are multiple ways to use this application. One of the most common +usage scenario is to run websocket-console application inside QEMU. This is +described in :ref:`networking_with_qemu`. + +Build ws_console sample application like this: + +.. zephyr-app-commands:: + :zephyr-app: samples/net/ws_console + :board: qemu_x86 + :goals: run + :compact: + +The default make BOARD configuration for this sample is ``qemu_x86``. + +Connect to the console from your browser using these URLs http://[2001:db8::1] +or http://192.0.2.1 as configured in the project's ``prj.conf`` file. diff --git a/samples/net/ws_console/prj.conf b/samples/net/ws_console/prj.conf new file mode 100644 index 0000000000000..4496a6a4dc27a --- /dev/null +++ b/samples/net/ws_console/prj.conf @@ -0,0 +1,58 @@ +# Generic IP stack options and features +CONFIG_NETWORKING=y +CONFIG_NET_UDP=n +CONFIG_NET_TCP=y +CONFIG_NET_IPV6=y +CONFIG_NET_IPV4=y +#CONFIG_NET_DHCPV4=y +CONFIG_ENTROPY_GENERATOR=y +CONFIG_TEST_RANDOM_GENERATOR=y +CONFIG_INIT_STACKS=y +CONFIG_NET_MAX_CONTEXTS=8 +CONFIG_NET_SHELL=y + +# Number of network buffers +CONFIG_NET_PKT_RX_COUNT=32 +CONFIG_NET_PKT_TX_COUNT=32 +CONFIG_NET_BUF_RX_COUNT=32 +CONFIG_NET_BUF_TX_COUNT=32 +CONFIG_NET_CONTEXT_NET_PKT_POOL=y + +# IPv6 address counts +CONFIG_NET_IF_UNICAST_IPV6_ADDR_COUNT=3 +CONFIG_NET_IF_MCAST_IPV6_ADDR_COUNT=4 + +# Network application settings +CONFIG_NET_APP=y +CONFIG_NET_APP_SETTINGS=y +CONFIG_NET_APP_NEED_IPV6=y +CONFIG_NET_APP_NEED_IPV4=y +CONFIG_NET_APP_MY_IPV6_ADDR="2001:db8::1" +CONFIG_NET_APP_MY_IPV4_ADDR="192.0.2.1" + +# HTTP & Websocket options +CONFIG_WEBSOCKET=y +CONFIG_WEBSOCKET_CONSOLE=y +CONFIG_HTTP=y +CONFIG_HTTPS=n +CONFIG_HTTP_SERVER=y +# How many URLs we are serving +CONFIG_HTTP_SERVER_NUM_URLS=5 + +# base64 support needed by websocket +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_CFG_FILE="config-mini-tls1_2.h" + +# Logging +CONFIG_NET_LOG=y +CONFIG_SYS_LOG_NET_LEVEL=2 +CONFIG_SYS_LOG_SHOW_COLOR=y +CONFIG_NET_STATISTICS=y +CONFIG_PRINTK=y + +# Debugging +CONFIG_NET_DEBUG_WEBSOCKET=y +CONFIG_NET_DEBUG_HTTP=n +CONFIG_NET_DEBUG_APP=n +CONFIG_NET_DEBUG_NET_PKT=y diff --git a/samples/net/ws_console/prj_tls.conf b/samples/net/ws_console/prj_tls.conf new file mode 100644 index 0000000000000..f8b1bd5980037 --- /dev/null +++ b/samples/net/ws_console/prj_tls.conf @@ -0,0 +1,60 @@ +# Generic IP stack options and features +CONFIG_NETWORKING=y +CONFIG_NET_UDP=n +CONFIG_NET_TCP=y +CONFIG_NET_IPV6=y +CONFIG_NET_IPV4=y +#CONFIG_NET_DHCPV4=y +CONFIG_ENTROPY_GENERATOR=y +CONFIG_TEST_RANDOM_GENERATOR=y +CONFIG_INIT_STACKS=y +CONFIG_NET_MAX_CONTEXTS=8 +CONFIG_NET_SHELL=y + +# Number of network buffers +CONFIG_NET_PKT_RX_COUNT=64 +CONFIG_NET_PKT_TX_COUNT=64 +CONFIG_NET_BUF_RX_COUNT=64 +CONFIG_NET_BUF_TX_COUNT=64 +CONFIG_NET_CONTEXT_NET_PKT_POOL=y + +# IPv6 address counts +CONFIG_NET_IF_UNICAST_IPV6_ADDR_COUNT=3 +CONFIG_NET_IF_MCAST_IPV6_ADDR_COUNT=4 + +# Network application settings +CONFIG_NET_APP=y +CONFIG_NET_APP_SETTINGS=y +CONFIG_NET_APP_NEED_IPV6=y +CONFIG_NET_APP_NEED_IPV4=y +CONFIG_NET_APP_MY_IPV6_ADDR="2001:db8::1" +CONFIG_NET_APP_MY_IPV4_ADDR="192.0.2.1" + +# HTTP & Websocket options +CONFIG_WEBSOCKET=y +CONFIG_WEBSOCKET_CONSOLE=y +CONFIG_HTTP=y +CONFIG_HTTPS=y +CONFIG_HTTP_SERVER=y +# How many URLs we are serving +CONFIG_HTTP_SERVER_NUM_URLS=5 + +# Crypto support +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_CFG_FILE="config-mini-tls1_2.h" +CONFIG_MBEDTLS_ENABLE_HEAP=y +CONFIG_MBEDTLS_HEAP_SIZE=30000 + +# Logging +CONFIG_NET_LOG=y +CONFIG_SYS_LOG_NET_LEVEL=2 +CONFIG_SYS_LOG_SHOW_COLOR=y +CONFIG_NET_STATISTICS=y +CONFIG_PRINTK=y + +# Debugging +CONFIG_NET_DEBUG_WEBSOCKET=y +CONFIG_NET_DEBUG_HTTP=n +CONFIG_NET_DEBUG_APP=n +CONFIG_NET_DEBUG_NET_PKT=y diff --git a/samples/net/ws_console/sample.yaml b/samples/net/ws_console/sample.yaml new file mode 100644 index 0000000000000..5865dc412d438 --- /dev/null +++ b/samples/net/ws_console/sample.yaml @@ -0,0 +1,7 @@ +sample: + name: Websocket console +tests: +- test: + build_only: true + platform_whitelist: qemu_x86 + tags: net websocket diff --git a/samples/net/ws_console/src/common.h b/samples/net/ws_console/src/common.h new file mode 100644 index 0000000000000..44132ba3843e7 --- /dev/null +++ b/samples/net/ws_console/src/common.h @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2017 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define MAX_DBG_PRINT 64 + +void start_ws_console(void); +void stop_ws_console(void); + +void quit(void); diff --git a/samples/net/ws_console/src/config.h b/samples/net/ws_console/src/config.h new file mode 100644 index 0000000000000..475ed0221bcdf --- /dev/null +++ b/samples/net/ws_console/src/config.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _CONFIG_H_ +#define _CONFIG_H_ + +/* The startup time needs to be longish if DHCP is enabled as setting + * DHCP up takes some time. + */ +#define APP_STARTUP_TIME K_SECONDS(20) + +#ifdef CONFIG_NET_APP_SETTINGS +#ifdef CONFIG_NET_IPV6 +#define ZEPHYR_ADDR CONFIG_NET_APP_MY_IPV6_ADDR +#else +#define ZEPHYR_ADDR CONFIG_NET_APP_MY_IPV4_ADDR +#endif +#else +#ifdef CONFIG_NET_IPV6 +#define ZEPHYR_ADDR "2001:db8::1" +#else +#define ZEPHYR_ADDR "192.0.2.1" +#endif +#endif + +#ifndef ZEPHYR_PORT +#define ZEPHYR_PORT 8080 +#endif + +#endif diff --git a/samples/net/ws_console/src/echo-apps-cert.der b/samples/net/ws_console/src/echo-apps-cert.der new file mode 100644 index 0000000000000..bfcb335e31c8c Binary files /dev/null and b/samples/net/ws_console/src/echo-apps-cert.der differ diff --git a/samples/net/ws_console/src/echo-apps-key.der b/samples/net/ws_console/src/echo-apps-key.der new file mode 100644 index 0000000000000..5a4d67372ea41 Binary files /dev/null and b/samples/net/ws_console/src/echo-apps-key.der differ diff --git a/samples/net/ws_console/src/favicon.ico b/samples/net/ws_console/src/favicon.ico new file mode 100644 index 0000000000000..6916039458a6e Binary files /dev/null and b/samples/net/ws_console/src/favicon.ico differ diff --git a/samples/net/ws_console/src/index.html b/samples/net/ws_console/src/index.html new file mode 100644 index 0000000000000..d7fd5167d4f9e --- /dev/null +++ b/samples/net/ws_console/src/index.html @@ -0,0 +1,137 @@ + + + + + + + Zephyr WS Console + + + + + +
+
+
+

+
+
+
+
+
+ + + + + +
+ + + + + +
+
+
+ + diff --git a/samples/net/ws_console/src/main.c b/samples/net/ws_console/src/main.c new file mode 100644 index 0000000000000..c0ccb6a05fae8 --- /dev/null +++ b/samples/net/ws_console/src/main.c @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2017 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#if 1 +#define SYS_LOG_DOMAIN "ws-console" +#define NET_SYS_LOG_LEVEL SYS_LOG_LEVEL_DEBUG +#define NET_LOG_ENABLED 1 +#endif + +#include +#include +#include + +#include +#include +#include + +#include "common.h" + +static struct k_sem quit_lock; + +void quit(void) +{ + k_sem_give(&quit_lock); +} + +static inline int init_app(void) +{ + k_sem_init(&quit_lock, 0, UINT_MAX); + + return 0; +} + +void main(void) +{ + init_app(); + + start_ws_console(); + + k_sem_take(&quit_lock, K_FOREVER); + + stop_ws_console(); +} diff --git a/samples/net/ws_console/src/style.css b/samples/net/ws_console/src/style.css new file mode 100644 index 0000000000000..021a027c39f26 --- /dev/null +++ b/samples/net/ws_console/src/style.css @@ -0,0 +1,54 @@ +body { + background-color: black; + color: white; +} + +table, th, td { + border: 1px solid black; + border-collapse: collapse; +} + +th, td { + padding: 5px; +} + +th { + text-align: left; +} + +div.container { +} + +div.textcontainer { + font-family: monospace !important; + font-size: 150%; +} + +div.zconsole { + white-space: pre-wrap !important; + scroll-behavior: auto; + border: 1px solid gray; + padding: 5px; + width: 95%; + height: 70%; + overflow: auto; +} + +div.output { + scroll-behavior: auto; + border: 1px solid gray; + padding: 5px; + margin-top: 20px; + width: 95%; + height: 10%; + overflow: auto; +} + +div.inputbar { + margin-top: 5px; + width: 100%; +} + +.zconsole em { + color: red; +} diff --git a/samples/net/ws_console/src/ws_console.c b/samples/net/ws_console/src/ws_console.c new file mode 100644 index 0000000000000..a790f037e4ec7 --- /dev/null +++ b/samples/net/ws_console/src/ws_console.c @@ -0,0 +1,479 @@ +/* + * Copyright (c) 2017 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#if 1 +#define SYS_LOG_DOMAIN "ws-console" +#define NET_SYS_LOG_LEVEL SYS_LOG_LEVEL_DEBUG +#define NET_LOG_ENABLED 1 +#endif + +/* Printing debugs from this module looks funny in console so do not + * do it unless you are debugging this module. + */ +#define EXTRA_DEBUG 0 + +#include +#include +#include + +#include +#include +#include + +#include + +#include "common.h" +#include "config.h" + +static struct http_ctx http_ctx; +static struct http_server_urls http_urls; + +#define MAX_URL_LEN 128 +#define SEND_TIMEOUT K_SECONDS(10) + +/* Note that both tcp and udp can share the same pool but in this + * example the UDP context and TCP context have separate pools. + */ +#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL) +NET_PKT_TX_SLAB_DEFINE(echo_tx_tcp, 15); +NET_PKT_DATA_POOL_DEFINE(echo_data_tcp, 30); + +static struct k_mem_slab *tx_tcp_slab(void) +{ + return &echo_tx_tcp; +} + +static struct net_buf_pool *data_tcp_pool(void) +{ + return &echo_data_tcp; +} +#else +#define tx_tcp_slab NULL +#define data_tcp_pool NULL +#endif /* CONFIG_NET_CONTEXT_NET_PKT_POOL */ + +/* The result buf size is set to large enough so that we can receive max size + * buf back. Note that mbedtls needs also be configured to have equal size + * value for its buffer size. See MBEDTLS_SSL_MAX_CONTENT_LEN option in TLS + * config file. + */ +#define RESULT_BUF_SIZE 1500 +static u8_t result[RESULT_BUF_SIZE]; + +#if defined(CONFIG_HTTPS) + +#if !defined(CONFIG_NET_APP_TLS_STACK_SIZE) +#define CONFIG_NET_APP_TLS_STACK_SIZE 8192 +#endif /* CONFIG_NET_APP_TLS_STACK_SIZE */ + +#define APP_BANNER "Run TLS ws-console" +#define INSTANCE_INFO "Zephyr TLS ws-console #1" + +/* Note that each net_app context needs its own stack as there will be + * a separate thread needed. + */ +NET_STACK_DEFINE(WS_TLS_CONSOLE, ws_tls_console_stack, + CONFIG_NET_APP_TLS_STACK_SIZE, CONFIG_NET_APP_TLS_STACK_SIZE); + +#define RX_FIFO_DEPTH 4 +K_MEM_POOL_DEFINE(ssl_pool, 4, 64, RX_FIFO_DEPTH, 4); +#endif /* CONFIG_HTTPS */ + +#if defined(CONFIG_HTTPS) +/* Load the certificates and private RSA key. */ + +static const char echo_apps_cert_der[] = { +#include "echo-apps-cert.der.inc" +}; + +static const char echo_apps_key_der[] = { +#include "echo-apps-key.der.inc" +}; + +static int setup_cert(struct net_app_ctx *ctx, + mbedtls_x509_crt *cert, + mbedtls_pk_context *pkey) +{ + int ret; + + ret = mbedtls_x509_crt_parse(cert, echo_apps_cert_der, + sizeof(echo_apps_cert_der)); + if (ret != 0) { + NET_ERR("mbedtls_x509_crt_parse returned %d", ret); + return ret; + } + + ret = mbedtls_pk_parse_key(pkey, echo_apps_key_der, + sizeof(echo_apps_key_der), NULL, 0); + if (ret != 0) { + NET_ERR("mbedtls_pk_parse_key returned %d", ret); + return ret; + } + + return 0; +} +#endif /* CONFIG_HTTPS */ + +#define HTTP_STATUS_200_OK "HTTP/1.1 200 OK\r\n" \ + "Content-Type: text/html\r\n" \ + "Transfer-Encoding: chunked\r\n" + +#define HTTP_STATUS_200_OK_GZ "HTTP/1.1 200 OK\r\n" \ + "Content-Type: text/html\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "Content-Encoding: gzip\r\n" + +#define HTTP_STATUS_200_OK_GZ_CSS \ + "HTTP/1.1 200 OK\r\n" \ + "Content-Type: text/css\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "Content-Encoding: gzip\r\n" + +#define HTTP_STATUS_200_OK_CSS \ + "HTTP/1.1 200 OK\r\n" \ + "Content-Type: text/css\r\n" \ + "Transfer-Encoding: chunked\r\n" + +#define HTML_HEADER "" \ + "Zephyr websocket console" \ + "

" \ + "
Zephyr websocket console

\r\n" + +#define HTML_FOOTER "\r\n" + +static int http_response(struct http_ctx *ctx, const char *header, + const char *payload, size_t payload_len, + const struct sockaddr *dst) +{ + char content_length[6]; + int ret; + + ret = http_add_header(ctx, header, dst, NULL); + if (ret < 0) { + NET_ERR("Cannot add HTTP header (%d)", ret); + return ret; + } + + ret = snprintk(content_length, sizeof(content_length), "%zd", + payload_len); + if (ret <= 0 || ret >= sizeof(content_length)) { + ret = -ENOMEM; + return ret; + } + + ret = http_add_header_field(ctx, "Content-Length", content_length, + dst, NULL); + if (ret < 0) { + NET_ERR("Cannot add Content-Length HTTP header (%d)", ret); + return ret; + } + + ret = http_add_header(ctx, HTTP_CRLF, dst, NULL); + if (ret < 0) { + return ret; + } + + ret = http_send_chunk(ctx, payload, payload_len, dst, NULL); + if (ret < 0) { + NET_ERR("Cannot send data to peer (%d)", ret); + return ret; + } + + return http_send_flush(ctx, NULL); +} + +static int http_response_soft_404(struct http_ctx *ctx, + const struct sockaddr *dst) +{ + static const char *not_found = + HTML_HEADER + "

404 Not Found

" + HTML_FOOTER; + + return http_response(ctx, HTTP_STATUS_200_OK, not_found, + strlen(not_found), dst); +} + +static int http_serve_index_html(struct http_ctx *ctx, + const struct sockaddr *dst) +{ + static const char index_html[] = { +#include "index.html.gz.inc" + }; + + NET_DBG("Sending index.html (%zd bytes) to client", + sizeof(index_html)); + + return http_response(ctx, HTTP_STATUS_200_OK_GZ, index_html, + sizeof(index_html), dst); +} + +static int http_serve_style_css(struct http_ctx *ctx, + const struct sockaddr *dst) +{ + static const char style_css_gz[] = { +#include "style.css.gz.inc" + }; + + NET_DBG("Sending style.css (%zd bytes) to client", + sizeof(style_css_gz)); + + return http_response(ctx, HTTP_STATUS_200_OK_GZ_CSS, + style_css_gz, sizeof(style_css_gz), + dst); +} + +static int http_serve_favicon_ico(struct http_ctx *ctx, + const struct sockaddr *dst) +{ + static const char favicon_ico_gz[] = { +#include "favicon.ico.gz.inc" + }; + + NET_DBG("Sending favicon.ico (%zd bytes) to client", + sizeof(favicon_ico_gz)); + + return http_response(ctx, HTTP_STATUS_200_OK_GZ, + favicon_ico_gz, sizeof(favicon_ico_gz), + dst); +} + +static void ws_connected(struct http_ctx *ctx, + enum http_connection_type type, + const struct sockaddr *dst, + void *user_data) +{ + char url[32]; + int len = min(sizeof(url), ctx->http.url_len); + + memcpy(url, ctx->http.url, len); + url[len] = '\0'; + + NET_DBG("%s connect attempt URL %s", + type == HTTP_CONNECTION ? "HTTP" : "WS", url); + + if (type == HTTP_CONNECTION) { + if (strncmp(ctx->http.url, "/index.html", + ctx->http.url_len) == 0) { + http_serve_index_html(ctx, dst); + http_close(ctx); + return; + } + + if (strncmp(ctx->http.url, "/style.css", + ctx->http.url_len) == 0) { + http_serve_style_css(ctx, dst); + http_close(ctx); + return; + } + + if (strncmp(ctx->http.url, "/favicon.ico", + ctx->http.url_len) == 0) { + http_serve_favicon_ico(ctx, dst); + http_close(ctx); + return; + } + + if (strncmp(ctx->http.url, "/", + ctx->http.url_len) == 0) { + http_serve_index_html(ctx, dst); + http_close(ctx); + return; + } + + } else if (type == WS_CONNECTION) { + if (strncmp(ctx->http.url, "/console", + ctx->http.url_len) == 0) { + ws_console_enable(ctx); + return; + } + } + + /* Give 404 error for all the other URLs we do not want to handle + * right now. + */ + http_response_soft_404(ctx, dst); + http_close(ctx); +} + +static void ws_received(struct http_ctx *ctx, + struct net_pkt *pkt, + int status, + u32_t flags, + const struct sockaddr *dst, + void *user_data) +{ + if (!status) { + int ret; + +#if EXTRA_DEBUG + NET_DBG("Received %d bytes data", net_pkt_appdatalen(pkt)); +#endif + + ret = ws_console_recv(ctx, pkt); + if (ret < 0) { + goto out; + } + } else { + NET_ERR("Receive error (%d)", status); + + goto out; + } + + return; + +out: + if (pkt) { + net_pkt_unref(pkt); + } +} + +static void ws_sent(struct http_ctx *ctx, + int status, + void *user_data_send, + void *user_data) +{ +#if EXTRA_DEBUG + NET_DBG("Data sent status %d", status); +#endif +} + +static void ws_closed(struct http_ctx *ctx, + int status, + void *user_data) +{ + NET_DBG("Connection %p closed", ctx); + + ws_console_disable(ctx); +} + +static const char *get_string(int str_len, const char *str) +{ + static char buf[64]; + int len = min(str_len, sizeof(buf) - 1); + + memcpy(buf, str, len); + buf[len] = '\0'; + + return buf; +} + +static enum http_verdict default_handler(struct http_ctx *ctx, + enum http_connection_type type, + const struct sockaddr *dst) +{ + NET_DBG("No handler for %s URL %s", + type == HTTP_CONNECTION ? "HTTP" : "WS", + get_string(ctx->http.url_len, ctx->http.url)); + + if (type == HTTP_CONNECTION) { + http_response_soft_404(ctx, dst); + } + + return HTTP_VERDICT_DROP; +} + +void start_ws_console(void) +{ + struct sockaddr addr, *server_addr; + int ret; + + /* + * There are several options here for binding to local address. + * 1) The server address can be left empty in which case the + * library will bind to both IPv4 and IPv6 addresses and to + * default port 80 or 443 if TLS is enabled. + * 2) The server address can be partially filled, meaning that + * the address can be left to 0 and port can be set to desired + * value. If the protocol family in sockaddr is set to AF_UNSPEC, + * then both IPv4 and IPv6 socket is bound. + * 3) The address can be set to some real value. + */ +#define ADDR_OPTION 1 + +#if ADDR_OPTION == 1 + server_addr = NULL; + + ARG_UNUSED(addr); + +#elif ADDR_OPTION == 2 + /* Accept any local listening address */ + memset(&addr, 0, sizeof(addr)); + + net_sin(&addr)->sin_port = htons(ZEPHYR_PORT); + + /* In this example, listen both IPv4 and IPv6 */ + addr.sa_family = AF_UNSPEC; + + server_addr = &addr; + +#elif ADDR_OPTION == 3 + /* Set the bind address according to your configuration */ + memset(&addr, 0, sizeof(addr)); + + /* In this example, listen only IPv6 */ + addr.sa_family = AF_INET6; + net_sin6(&addr)->sin6_port = htons(ZEPHYR_PORT); + + ret = net_ipaddr_parse(ZEPHYR_ADDR, &addr); + if (ret < 0) { + NET_ERR("Cannot set local address (%d)", ret); + panic(NULL); + } + + server_addr = &addr; + +#else + server_addr = NULL; + + ARG_UNUSED(addr); +#endif + + http_server_add_default(&http_urls, default_handler); + http_server_add_url(&http_urls, "/", HTTP_URL_STANDARD); + http_server_add_url(&http_urls, "/index.html", HTTP_URL_STANDARD); + http_server_add_url(&http_urls, "/style.css", HTTP_URL_STANDARD); + http_server_add_url(&http_urls, "/favicon.ico", HTTP_URL_STANDARD); + http_server_add_url(&http_urls, "/console", HTTP_URL_WEBSOCKET); + + ret = http_server_init(&http_ctx, &http_urls, server_addr, + result, sizeof(result), + "Zephyr WS console", NULL); + if (ret < 0) { + NET_ERR("Cannot init web server (%d)", ret); + return; + } + + http_set_cb(&http_ctx, ws_connected, ws_received, ws_sent, ws_closed); + +#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL) + net_app_set_net_pkt_pool(&http_ctx.app_ctx, tx_tcp_slab, + data_tcp_pool); +#endif + +#if defined(CONFIG_HTTPS) + ret = http_server_set_tls(&http_ctx, + APP_BANNER, + INSTANCE_INFO, + strlen(INSTANCE_INFO), + setup_cert, + NULL, + &ssl_pool, + ws_tls_console_stack, + K_THREAD_STACK_SIZEOF(ws_tls_console_stack)); + if (ret < 0) { + NET_ERR("Cannot enable TLS support (%d)", ret); + } +#endif + + http_server_enable(&http_ctx); +} + +void stop_ws_console(void) +{ + http_server_disable(&http_ctx); + http_release(&http_ctx); +} diff --git a/samples/net/ws_echo_server/CMakeLists.txt b/samples/net/ws_echo_server/CMakeLists.txt new file mode 100644 index 0000000000000..d2b23c8f442b7 --- /dev/null +++ b/samples/net/ws_echo_server/CMakeLists.txt @@ -0,0 +1,27 @@ +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(NONE) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) + +include($ENV{ZEPHYR_BASE}/samples/net/common/common.cmake) + +set(gen_dir ${ZEPHYR_BINARY_DIR}/include/generated/) + +# List of files that are used to generate .h file that can be included +# into .c file. +foreach(inc_file + echo-apps-cert.der + echo-apps-key.der + index.html + ) + generate_inc_file_for_target( + app + src/${inc_file} + ${gen_dir}/${inc_file}.inc + ) +endforeach() + +generate_inc_file_for_target(app src/ws.js ${gen_dir}/ws.js.gz.inc --gzip) + +target_link_libraries_ifdef(CONFIG_MBEDTLS app mbedTLS) diff --git a/samples/net/ws_echo_server/README.rst b/samples/net/ws_echo_server/README.rst new file mode 100644 index 0000000000000..74ba03a46c8dc --- /dev/null +++ b/samples/net/ws_echo_server/README.rst @@ -0,0 +1,40 @@ +.. _websocket-server-sample: + +Websocket Server +################ + +Overview +******** + +The websocket-server sample application for Zephyr implements a websocket +server. The websocket-server listens for incoming IPv4 or IPv6 HTTP(S) +requests and sends back the same data. + +The source code for this sample application can be found at: +:file:`samples/net/ws_server`. + +Requirements +************ + +- :ref:`networking_with_qemu` + +Building and Running +******************** + +There are multiple ways to use this application. One of the most common +usage scenario is to run websocket-server application inside QEMU. This is +described in :ref:`networking_with_qemu`. + +Build websocket-server sample application like this: + +.. zephyr-app-commands:: + :zephyr-app: samples/net/ws_echo_server + :board: qemu_x86 + :goals: run + :compact: + +The default make BOARD configuration for this sample is ``qemu_x86``. + +Connect to the websocket server from your browser using these URLs +http://[2001:db8::1] or http://192.0.2.1 as configured in the project's +``prj.conf`` file. diff --git a/samples/net/ws_echo_server/prj.conf b/samples/net/ws_echo_server/prj.conf new file mode 100644 index 0000000000000..4d9c41b355c63 --- /dev/null +++ b/samples/net/ws_echo_server/prj.conf @@ -0,0 +1,56 @@ +# Generic IP stack options and features +CONFIG_NETWORKING=y +CONFIG_NET_UDP=n +CONFIG_NET_TCP=y +CONFIG_NET_IPV6=y +CONFIG_NET_IPV4=y +#CONFIG_NET_DHCPV4=y +CONFIG_ENTROPY_GENERATOR=y +CONFIG_TEST_RANDOM_GENERATOR=y +CONFIG_INIT_STACKS=y +CONFIG_NET_MAX_CONTEXTS=8 +CONFIG_NET_SHELL=y + +# Number of network buffers +CONFIG_NET_PKT_RX_COUNT=16 +CONFIG_NET_PKT_TX_COUNT=16 +CONFIG_NET_BUF_RX_COUNT=16 +CONFIG_NET_BUF_TX_COUNT=16 +CONFIG_NET_CONTEXT_NET_PKT_POOL=y + +# IPv6 address counts +CONFIG_NET_IF_UNICAST_IPV6_ADDR_COUNT=3 +CONFIG_NET_IF_MCAST_IPV6_ADDR_COUNT=4 + +# Network application settings +CONFIG_NET_APP=y +CONFIG_NET_APP_SETTINGS=y +CONFIG_NET_APP_NEED_IPV6=y +CONFIG_NET_APP_NEED_IPV4=y +CONFIG_NET_APP_MY_IPV6_ADDR="2001:db8::1" +CONFIG_NET_APP_MY_IPV4_ADDR="192.0.2.1" + +# HTTP & Websocket options +CONFIG_WEBSOCKET=y +CONFIG_HTTP=y +CONFIG_HTTPS=n +CONFIG_HTTP_SERVER=y +# How many URLs we are serving +CONFIG_HTTP_SERVER_NUM_URLS=5 + +# base64 support needed by websocket +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_CFG_FILE="config-mini-tls1_2.h" + +# Logging +CONFIG_NET_LOG=y +CONFIG_SYS_LOG_NET_LEVEL=4 +CONFIG_SYS_LOG_SHOW_COLOR=y +CONFIG_NET_STATISTICS=y + +# Debugging +CONFIG_NET_DEBUG_WEBSOCKET=y +CONFIG_NET_DEBUG_HTTP=y +CONFIG_NET_DEBUG_APP=n +CONFIG_NET_DEBUG_NET_PKT=y diff --git a/samples/net/ws_echo_server/prj_tls.conf b/samples/net/ws_echo_server/prj_tls.conf new file mode 100644 index 0000000000000..f73bef7b9759b --- /dev/null +++ b/samples/net/ws_echo_server/prj_tls.conf @@ -0,0 +1,59 @@ +# Generic IP stack options and features +CONFIG_NETWORKING=y +CONFIG_NET_UDP=n +CONFIG_NET_TCP=y +CONFIG_NET_IPV6=y +CONFIG_NET_IPV4=y +#CONFIG_NET_DHCPV4=y +CONFIG_ENTROPY_GENERATOR=y +CONFIG_TEST_RANDOM_GENERATOR=y +CONFIG_INIT_STACKS=y +CONFIG_NET_MAX_CONTEXTS=8 +CONFIG_NET_SHELL=y + +# Number of network buffers +CONFIG_NET_PKT_RX_COUNT=64 +CONFIG_NET_PKT_TX_COUNT=64 +CONFIG_NET_BUF_RX_COUNT=64 +CONFIG_NET_BUF_TX_COUNT=64 +CONFIG_NET_CONTEXT_NET_PKT_POOL=y + +# IPv6 address counts +CONFIG_NET_IF_UNICAST_IPV6_ADDR_COUNT=3 +CONFIG_NET_IF_MCAST_IPV6_ADDR_COUNT=4 + +# Network application settings +CONFIG_NET_APP=y +CONFIG_NET_APP_SETTINGS=y +CONFIG_NET_APP_NEED_IPV6=y +CONFIG_NET_APP_NEED_IPV4=y +CONFIG_NET_APP_MY_IPV6_ADDR="2001:db8::1" +CONFIG_NET_APP_MY_IPV4_ADDR="192.0.2.1" + +# HTTP & Websocket options +CONFIG_WEBSOCKET=y +CONFIG_HTTP=y +CONFIG_HTTPS=y +CONFIG_HTTP_SERVER=y +# How many URLs we are serving +CONFIG_HTTP_SERVER_NUM_URLS=5 + +# Crypto support +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_CFG_FILE="config-mini-tls1_2.h" +CONFIG_MBEDTLS_ENABLE_HEAP=y +CONFIG_MBEDTLS_HEAP_SIZE=30000 + +# Logging +CONFIG_NET_LOG=y +CONFIG_SYS_LOG_NET_LEVEL=2 +CONFIG_SYS_LOG_SHOW_COLOR=y +CONFIG_NET_STATISTICS=y +CONFIG_PRINTK=y + +# Debugging +CONFIG_NET_DEBUG_WEBSOCKET=y +CONFIG_NET_DEBUG_HTTP=n +CONFIG_NET_DEBUG_APP=n +CONFIG_NET_DEBUG_NET_PKT=y diff --git a/samples/net/ws_echo_server/sample.yaml b/samples/net/ws_echo_server/sample.yaml new file mode 100644 index 0000000000000..1556a1a6576bf --- /dev/null +++ b/samples/net/ws_echo_server/sample.yaml @@ -0,0 +1,7 @@ +sample: + name: Websocket echo-server +tests: +- test: + build_only: true + platform_whitelist: qemu_x86 qemu_cortex_m3 + tags: net websocket diff --git a/samples/net/ws_echo_server/src/Makefile b/samples/net/ws_echo_server/src/Makefile new file mode 100644 index 0000000000000..900b44acafbe4 --- /dev/null +++ b/samples/net/ws_echo_server/src/Makefile @@ -0,0 +1,24 @@ +# +# Copyright (c) 2017 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 +# + +# List of files that are used to generate .h file that can be included +# into .c file. +generate_inc_file += \ + echo-apps-cert.der \ + echo-apps-key.der + +generate_inc_file += \ + index.html + +generate_inc_gz_file += \ + ws.js + +include $(ZEPHYR_BASE)/scripts/Makefile.gen + +include $(ZEPHYR_BASE)/samples/net/common/Makefile.common + +obj-y += main.o +obj-y += ws.o diff --git a/samples/net/ws_echo_server/src/common.h b/samples/net/ws_echo_server/src/common.h new file mode 100644 index 0000000000000..3274700f9d316 --- /dev/null +++ b/samples/net/ws_echo_server/src/common.h @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2017 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define MAX_DBG_PRINT 64 + +void start_server(void); +void stop_server(void); + +struct net_pkt *build_reply_pkt(const char *name, + struct net_app_ctx *ctx, + struct net_pkt *pkt); +void pkt_sent(struct net_app_ctx *ctx, int status, + void *token, void *user_data); +void panic(const char *msg); +void quit(void); diff --git a/samples/net/ws_echo_server/src/config.h b/samples/net/ws_echo_server/src/config.h new file mode 100644 index 0000000000000..475ed0221bcdf --- /dev/null +++ b/samples/net/ws_echo_server/src/config.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2017 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _CONFIG_H_ +#define _CONFIG_H_ + +/* The startup time needs to be longish if DHCP is enabled as setting + * DHCP up takes some time. + */ +#define APP_STARTUP_TIME K_SECONDS(20) + +#ifdef CONFIG_NET_APP_SETTINGS +#ifdef CONFIG_NET_IPV6 +#define ZEPHYR_ADDR CONFIG_NET_APP_MY_IPV6_ADDR +#else +#define ZEPHYR_ADDR CONFIG_NET_APP_MY_IPV4_ADDR +#endif +#else +#ifdef CONFIG_NET_IPV6 +#define ZEPHYR_ADDR "2001:db8::1" +#else +#define ZEPHYR_ADDR "192.0.2.1" +#endif +#endif + +#ifndef ZEPHYR_PORT +#define ZEPHYR_PORT 8080 +#endif + +#endif diff --git a/samples/net/ws_echo_server/src/echo-apps-cert.der b/samples/net/ws_echo_server/src/echo-apps-cert.der new file mode 100644 index 0000000000000..bfcb335e31c8c Binary files /dev/null and b/samples/net/ws_echo_server/src/echo-apps-cert.der differ diff --git a/samples/net/ws_echo_server/src/echo-apps-key.der b/samples/net/ws_echo_server/src/echo-apps-key.der new file mode 100644 index 0000000000000..5a4d67372ea41 Binary files /dev/null and b/samples/net/ws_echo_server/src/echo-apps-key.der differ diff --git a/samples/net/ws_echo_server/src/index.html b/samples/net/ws_echo_server/src/index.html new file mode 100644 index 0000000000000..da6482eab4b9c --- /dev/null +++ b/samples/net/ws_echo_server/src/index.html @@ -0,0 +1,20 @@ + + + + + Zephyr HTTP websocket server sample + + + +
+
+

Zephyr HTTP websocket server sample

+
+
+

HTTP connection ok.

+
+
+
+
+ + diff --git a/samples/net/ws_echo_server/src/main.c b/samples/net/ws_echo_server/src/main.c new file mode 100644 index 0000000000000..e008a1dd0a3b8 --- /dev/null +++ b/samples/net/ws_echo_server/src/main.c @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2016 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#if 1 +#define SYS_LOG_DOMAIN "ws-server" +#define NET_SYS_LOG_LEVEL SYS_LOG_LEVEL_DEBUG +#define NET_LOG_ENABLED 1 +#endif + +#include +#include +#include + +#include +#include +#include + +#include + +#include "common.h" + +/* The startup time needs to be longish if DHCP is enabled as setting + * DHCP up takes some time. + */ +#define APP_STARTUP_TIME K_SECONDS(20) + +#define APP_BANNER "Run websocket server" + +static struct k_sem quit_lock; + +void panic(const char *msg) +{ + if (msg) { + NET_ERR("%s", msg); + } + + for (;;) { + k_sleep(K_FOREVER); + } +} + +void quit(void) +{ + k_sem_give(&quit_lock); +} + +struct net_pkt *build_reply_pkt(const char *name, + struct net_app_ctx *ctx, + struct net_pkt *pkt) +{ + struct net_pkt *reply_pkt; + struct net_buf *frag, *tmp; + int header_len = 0, recv_len, reply_len; + + NET_INFO("%s received %d bytes", name, net_pkt_appdatalen(pkt)); + + if (net_pkt_appdatalen(pkt) == 0) { + return NULL; + } + + reply_pkt = net_app_get_net_pkt(ctx, net_pkt_family(pkt), K_FOREVER); + + NET_ASSERT(reply_pkt); + NET_ASSERT(net_pkt_family(reply_pkt) == net_pkt_family(pkt)); + + recv_len = net_pkt_get_len(pkt); + + tmp = pkt->frags; + + /* If we have link layer headers, then get rid of them here. */ + if (recv_len != net_pkt_appdatalen(pkt)) { + /* First fragment will contain IP header so move the data + * down in order to get rid of it. + */ + header_len = net_pkt_appdata(pkt) - tmp->data; + + NET_ASSERT(header_len < CONFIG_NET_BUF_DATA_SIZE); + + /* After this pull, the tmp->data points directly to application + * data. + */ + net_buf_pull(tmp, header_len); + } + + net_pkt_set_appdatalen(reply_pkt, net_pkt_appdatalen(pkt)); + + while (tmp) { + frag = net_app_get_net_buf(ctx, reply_pkt, K_FOREVER); + + if (net_buf_headroom(tmp) == 0) { + /* If there is no link layer headers in the + * received fragment, then get rid of that also + * in the sending fragment. We end up here + * if MTU is larger than fragment size, this + * is typical for ethernet. + */ + net_buf_push(frag, net_buf_headroom(frag)); + + frag->len = 0; /* to make fragment empty */ + + /* Make sure to set the reserve so that + * in sending side we add the link layer + * header if needed. + */ + net_pkt_set_ll_reserve(reply_pkt, 0); + } + + NET_ASSERT_INFO(net_buf_tailroom(frag) >= tmp->len, + "tail %zd longer than len %d", + net_buf_tailroom(frag), tmp->len); + + memcpy(net_buf_add(frag, tmp->len), tmp->data, tmp->len); + + tmp = net_pkt_frag_del(pkt, NULL, tmp); + } + + reply_len = net_pkt_get_len(reply_pkt); + + NET_ASSERT_INFO((recv_len - header_len) == reply_len, + "Received %d bytes, sending %d bytes", + recv_len - header_len, reply_len); + + return reply_pkt; +} + +void pkt_sent(struct net_app_ctx *ctx, + int status, + void *user_data_send, + void *user_data) +{ + if (!status) { + NET_INFO("Sent %d bytes", POINTER_TO_UINT(user_data_send)); + } +} + +static inline int init_app(void) +{ + k_sem_init(&quit_lock, 0, UINT_MAX); + + return 0; +} + +void main(void) +{ + init_app(); + + if (IS_ENABLED(CONFIG_NET_TCP)) { + start_server(); + } + + k_sem_take(&quit_lock, K_FOREVER); + + NET_INFO("Stopping..."); + + if (IS_ENABLED(CONFIG_NET_TCP)) { + stop_server(); + } +} diff --git a/samples/net/ws_echo_server/src/ws.c b/samples/net/ws_echo_server/src/ws.c new file mode 100644 index 0000000000000..252f2c616cc20 --- /dev/null +++ b/samples/net/ws_echo_server/src/ws.c @@ -0,0 +1,533 @@ +/* + * Copyright (c) 2017 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#if 1 +#define SYS_LOG_DOMAIN "ws-echo-server" +#define NET_SYS_LOG_LEVEL SYS_LOG_LEVEL_DEBUG +#define NET_LOG_ENABLED 1 +#endif + +#include +#include +#include + +#include +#include +#include + +#include + +#include "common.h" +#include "config.h" + +#define MAX_BUF_LEN 128 +#define MAX_URL_LEN 128 +#define SEND_TIMEOUT K_SECONDS(10) +#define ALLOC_TIMEOUT 100 + +static struct http_ctx http_ctx; +static struct http_server_urls http_urls; +static char buf[MAX_BUF_LEN]; +static int msg_count; + +/* Note that both tcp and udp can share the same pool but in this + * example the UDP context and TCP context have separate pools. + */ +#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL) +NET_PKT_TX_SLAB_DEFINE(echo_tx_tcp, 15); +NET_PKT_DATA_POOL_DEFINE(echo_data_tcp, 30); + +static struct k_mem_slab *tx_tcp_slab(void) +{ + return &echo_tx_tcp; +} + +static struct net_buf_pool *data_tcp_pool(void) +{ + return &echo_data_tcp; +} +#else +#define tx_tcp_slab NULL +#define data_tcp_pool NULL +#endif /* CONFIG_NET_CONTEXT_NET_PKT_POOL */ + +/* The result buf size is set to large enough so that we can receive max size + * buf back. Note that mbedtls needs also be configured to have equal size + * value for its buffer size. See MBEDTLS_SSL_MAX_CONTENT_LEN option in TLS + * config file. + */ +#define RESULT_BUF_SIZE 1500 +static u8_t result[RESULT_BUF_SIZE]; + +#if defined(CONFIG_HTTPS) + +#if !defined(CONFIG_NET_APP_TLS_STACK_SIZE) +#define CONFIG_NET_APP_TLS_STACK_SIZE 8192 +#endif /* CONFIG_NET_APP_TLS_STACK_SIZE */ + +#define APP_BANNER "Run TLS ws-server" +#define INSTANCE_INFO "Zephyr TLS ws-server #1" + +/* Note that each net_app context needs its own stack as there will be + * a separate thread needed. + */ +NET_STACK_DEFINE(WS_ECHO_SERVER, ws_tls_stack, + CONFIG_NET_APP_TLS_STACK_SIZE, CONFIG_NET_APP_TLS_STACK_SIZE); + +#define RX_FIFO_DEPTH 4 +K_MEM_POOL_DEFINE(ssl_pool, 4, 64, RX_FIFO_DEPTH, 4); +#endif /* CONFIG_HTTPS */ + +#if defined(CONFIG_HTTPS) +/* Load the certificates and private RSA key. */ + +static const char echo_apps_cert_der[] = { +#include "echo-apps-cert.der.inc" +}; + +static const char echo_apps_key_der[] = { +#include "echo-apps-key.der.inc" +}; + +static int setup_cert(struct net_app_ctx *ctx, + mbedtls_x509_crt *cert, + mbedtls_pk_context *pkey) +{ + int ret; + + ret = mbedtls_x509_crt_parse(cert, echo_apps_cert_der, + sizeof(echo_apps_cert_der)); + if (ret != 0) { + NET_ERR("mbedtls_x509_crt_parse returned %d", ret); + return ret; + } + + ret = mbedtls_pk_parse_key(pkey, echo_apps_key_der, + sizeof(echo_apps_key_der), NULL, 0); + if (ret != 0) { + NET_ERR("mbedtls_pk_parse_key returned %d", ret); + return ret; + } + + return 0; +} +#endif /* CONFIG_HTTPS */ + +#define HTTP_STATUS_200_OK "HTTP/1.1 200 OK\r\n" \ + "Content-Type: text/html\r\n" \ + "Transfer-Encoding: chunked\r\n" + +#define HTTP_STATUS_200_OK_GZ_CSS \ + "HTTP/1.1 200 OK\r\n" \ + "Content-Type: text/css\r\n" \ + "Transfer-Encoding: chunked\r\n" \ + "Content-Encoding: gzip\r\n" + +#define HTML_HEADER "" \ + "Zephyr HTTP Server" \ + "

" \ + "
Zephyr HTTP websocket server

\r\n" + +#define HTML_FOOTER "\r\n" + +static int http_response(struct http_ctx *ctx, const char *header, + const char *payload, size_t payload_len, + const struct sockaddr *dst) +{ + char content_length[6]; + int ret; + + ret = http_add_header(ctx, header, dst, NULL); + if (ret < 0) { + NET_ERR("Cannot add HTTP header (%d)", ret); + return ret; + } + + ret = snprintk(content_length, sizeof(content_length), "%zd", + payload_len); + if (ret <= 0 || ret >= sizeof(content_length)) { + ret = -ENOMEM; + return ret; + } + + ret = http_add_header_field(ctx, "Content-Length", content_length, + dst, NULL); + if (ret < 0) { + NET_ERR("Cannot add Content-Length HTTP header (%d)", ret); + return ret; + } + + ret = http_add_header(ctx, HTTP_CRLF, dst, NULL); + if (ret < 0) { + return ret; + } + + ret = http_send_chunk(ctx, payload, payload_len, dst, NULL); + if (ret < 0) { + NET_ERR("Cannot send data to peer (%d)", ret); + return ret; + } + + return http_send_flush(ctx, NULL); +} + +static int http_serve_index_html(struct http_ctx *ctx, + const struct sockaddr *dst) +{ + static const char index_html[] = { +#include "index.html.inc" + }; + + NET_DBG("Sending index.html (%zd bytes) to client", + sizeof(index_html)); + + return http_response(ctx, HTTP_STATUS_200_OK, index_html, + sizeof(index_html), dst); +} + +static int http_serve_js(struct http_ctx *ctx, + const struct sockaddr *dst) +{ + static const char js_gz[] = { +#include "ws.js.gz.inc" + }; + + NET_DBG("Sending ws.js (%zd bytes) to client", sizeof(js_gz)); + return http_response(ctx, HTTP_STATUS_200_OK_GZ_CSS, + js_gz, sizeof(js_gz), dst); +} + +static int http_response_soft_404(struct http_ctx *ctx, + const struct sockaddr *dst) +{ + static const char *not_found = + HTML_HEADER + "

404 Not Found

" + HTML_FOOTER; + + return http_response(ctx, HTTP_STATUS_200_OK, not_found, + strlen(not_found), dst); +} + +static int append_and_send_data(struct http_ctx *http_ctx, + const struct sockaddr *dst, + bool final, const char *fmt, ...) +{ + enum ws_opcode opcode = WS_OPCODE_CONTINUE; + va_list ap; + size_t len; + int ret; + + va_start(ap, fmt); + vsnprintk(buf, MAX_BUF_LEN, fmt, ap); + va_end(ap); + + len = strlen(buf); + + if (final) { + if (msg_count == 0) { + opcode = WS_OPCODE_DATA_TEXT; + } + + ret = ws_send_msg_to_client(http_ctx, buf, len, + opcode, final, dst, NULL); + if (ret < 0) { + NET_DBG("Could not send %zd bytes data to client", + len); + goto out; + } else { + NET_DBG("Sent %zd bytes to client", len); + } + + msg_count = 0; + + return ret; + } + + if (msg_count == 0) { + opcode = WS_OPCODE_DATA_TEXT; + } + + ret = ws_send_msg_to_client(http_ctx, buf, len, + opcode, final, dst, NULL); + if (ret < 0) { + NET_DBG("Could not send %zd bytes data to client", len); + goto out; + } else { + NET_DBG("Sent %zd bytes to client", len); + } + + msg_count++; + +out: + return ret; +} + +static int ws_works(struct http_ctx *ctx, + const struct sockaddr *dst) +{ + int ret = 0; + + NET_INFO("WS url called"); + + append_and_send_data(ctx, dst, false, "connection"); + append_and_send_data(ctx, dst, true, " established."); + + return ret; +} + +static void ws_connected(struct http_ctx *ctx, + enum http_connection_type type, + const struct sockaddr *dst, + void *user_data) +{ + char url[32]; + int len = min(sizeof(url), ctx->http.url_len); + + memcpy(url, ctx->http.url, len); + url[len] = '\0'; + + NET_DBG("%s connect attempt URL %s", + type == HTTP_CONNECTION ? "HTTP" : "WS", url); + + if (type == HTTP_CONNECTION) { + if (strncmp(ctx->http.url, "/index.html", + sizeof("/index.html") - 1) == 0) { + http_serve_index_html(ctx, dst); + http_close(ctx); + return; + } + + if (strncmp(ctx->http.url, "/ws.js", + sizeof("/ws.js") - 1) == 0) { + http_serve_js(ctx, dst); + http_close(ctx); + return; + } + + if (strncmp(ctx->http.url, "/", + ctx->http.url_len) == 0) { + http_serve_index_html(ctx, dst); + http_close(ctx); + return; + } + + } else if (type == WS_CONNECTION) { + if (strncmp(ctx->http.url, "/ws", + sizeof("/ws") - 1) == 0) { + ws_works(ctx, dst); + return; + } + } + + /* Give 404 error for all the other URLs we do not want to handle + * right now. + */ + http_response_soft_404(ctx, dst); + http_close(ctx); +} + +static void ws_received(struct http_ctx *ctx, + struct net_pkt *pkt, + int status, + u32_t flags, + const struct sockaddr *dst, + void *user_data) +{ + if (!status) { + struct net_buf *frag; + enum ws_opcode opcode; + int ret, hdr_len; + + NET_DBG("Received %d bytes data", net_pkt_appdatalen(pkt)); + + if (flags & WS_FLAG_BINARY) { + opcode = WS_OPCODE_DATA_BINARY; + } else { + opcode = WS_OPCODE_DATA_TEXT; + } + + hdr_len = net_pkt_get_len(pkt) - net_pkt_appdatalen(pkt); + + frag = pkt->frags; + while (frag) { + ret = ws_send_msg_to_client(ctx, frag->data + hdr_len, + frag->len - hdr_len, + opcode, + frag->frags ? true : false, + dst, user_data); + if (ret < 0) { + NET_DBG("Cannot send ws data (%d bytes) " + "back (%d)", + frag->len - hdr_len, ret); + } else { + NET_DBG("Sent %d bytes to client", + frag->len - hdr_len); + } + + frag = frag->frags; + + /* Websocket header is found in first fragment so + * reset the value here. + */ + hdr_len = 0; + } + + http_send_flush(ctx, user_data); + + if (pkt) { + net_pkt_unref(pkt); + } + + } else { + NET_ERR("Receive error (%d)", status); + + if (pkt) { + net_pkt_unref(pkt); + } + } +} + +static void ws_sent(struct http_ctx *ctx, + int status, + void *user_data_send, + void *user_data) +{ + NET_DBG("Data sent status %d", status); +} + +static void ws_closed(struct http_ctx *ctx, + int status, + void *user_data) +{ + NET_DBG("Connection %p closed", ctx); +} + +static const char *get_string(int str_len, const char *str) +{ + static char buf[64]; + int len = min(str_len, sizeof(buf) - 1); + + memcpy(buf, str, len); + buf[len] = '\0'; + + return buf; +} + +static enum http_verdict default_handler(struct http_ctx *ctx, + enum http_connection_type type, + const struct sockaddr *dst) +{ + NET_DBG("No handler for %s URL %s", + type == HTTP_CONNECTION ? "HTTP" : "WS", + get_string(ctx->http.url_len, ctx->http.url)); + + if (type == HTTP_CONNECTION) { + http_response_soft_404(ctx, dst); + } + + return HTTP_VERDICT_DROP; +} + +void start_server(void) +{ + struct sockaddr addr, *server_addr; + int ret; + + /* + * There are several options here for binding to local address. + * 1) The server address can be left empty in which case the + * library will bind to both IPv4 and IPv6 addresses and to + * default port 80 or 443 if TLS is enabled. + * 2) The server address can be partially filled, meaning that + * the address can be left to 0 and port can be set to desired + * value. If the protocol family in sockaddr is set to AF_UNSPEC, + * then both IPv4 and IPv6 socket is bound. + * 3) The address can be set to some real value. + */ +#define ADDR_OPTION 1 + +#if ADDR_OPTION == 1 + server_addr = NULL; + + ARG_UNUSED(addr); + +#elif ADDR_OPTION == 2 + /* Accept any local listening address */ + memset(&addr, 0, sizeof(addr)); + + net_sin(&addr)->sin_port = htons(ZEPHYR_PORT); + + /* In this example, listen both IPv4 and IPv6 */ + addr.sa_family = AF_UNSPEC; + + server_addr = &addr; + +#elif ADDR_OPTION == 3 + /* Set the bind address according to your configuration */ + memset(&addr, 0, sizeof(addr)); + + /* In this example, listen only IPv6 */ + addr.sa_family = AF_INET6; + net_sin6(&addr)->sin6_port = htons(ZEPHYR_PORT); + + ret = net_ipaddr_parse(ZEPHYR_ADDR, &addr); + if (ret < 0) { + NET_ERR("Cannot set local address (%d)", ret); + panic(NULL); + } + + server_addr = &addr; + +#else + server_addr = NULL; + + ARG_UNUSED(addr); +#endif + + http_server_add_default(&http_urls, default_handler); + http_server_add_url(&http_urls, "/index.html", HTTP_URL_STANDARD); + http_server_add_url(&http_urls, "/ws.js", HTTP_URL_STANDARD); + http_server_add_url(&http_urls, "/", HTTP_URL_STANDARD); + http_server_add_url(&http_urls, "/ws", HTTP_URL_WEBSOCKET); + + ret = http_server_init(&http_ctx, &http_urls, server_addr, + result, sizeof(result), + "Zephyr WS server", NULL); + if (ret < 0) { + NET_ERR("Cannot init web server (%d)", ret); + return; + } + + http_set_cb(&http_ctx, ws_connected, ws_received, ws_sent, ws_closed); + +#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL) + net_app_set_net_pkt_pool(&http_ctx.app_ctx, tx_tcp_slab, data_tcp_pool); +#endif + +#if defined(CONFIG_NET_APP_TLS) + ret = http_server_set_tls(&http_ctx, + APP_BANNER, + INSTANCE_INFO, + strlen(INSTANCE_INFO), + setup_cert, + NULL, + &ssl_pool, + ws_tls_stack, + K_THREAD_STACK_SIZEOF(ws_tls_stack)); + if (ret < 0) { + NET_ERR("Cannot enable TLS support (%d)", ret); + } +#endif + + http_server_enable(&http_ctx); +} + +void stop_server(void) +{ + http_server_disable(&http_ctx); + http_release(&http_ctx); +} diff --git a/samples/net/ws_echo_server/src/ws.js b/samples/net/ws_echo_server/src/ws.js new file mode 100644 index 0000000000000..ed207177a8a11 --- /dev/null +++ b/samples/net/ws_echo_server/src/ws.js @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +var connected; +var first_run; +var ws; + +function init() { + ws = new WebSocket(location.origin.replace("http", "ws") + "/ws"); + + first_run = "true"; + connected = "false"; + + ws.onopen = function() { + output("Websocket connection opened"); + connected = "true"; + }; + + ws.onmessage = function(e) { + output("Websocket data received: " + e.data); + }; + + ws.onclose = function() { + output("Websocket connection closed"); + connected = "false"; + }; + + ws.onerror = function(e) { + output("Websocket data error (see console)"); + console.log(e) + }; +} + +function escape(str) { + return str.replace(/&/, "&").replace(//, ">").replace(/"/, """); // " +} + +function output(str) { + var log = document.getElementById("output"); + log.innerHTML += escape(str) + "
"; +} diff --git a/subsys/net/lib/CMakeLists.txt b/subsys/net/lib/CMakeLists.txt index 48990f1856508..5296b3aa08e85 100644 --- a/subsys/net/lib/CMakeLists.txt +++ b/subsys/net/lib/CMakeLists.txt @@ -5,6 +5,7 @@ add_subdirectory_ifdef(CONFIG_DNS_RESOLVER dns) add_subdirectory_ifdef(CONFIG_MQTT_LIB mqtt) add_subdirectory_ifdef(CONFIG_NET_APP_SETTINGS app) add_subdirectory_ifdef(CONFIG_NET_SOCKETS sockets) +add_subdirectory_ifdef(CONFIG_WEBSOCKET websocket) if(CONFIG_HTTP_PARSER_URL OR CONFIG_HTTP_PARSER diff --git a/subsys/net/lib/Kconfig b/subsys/net/lib/Kconfig index ebe3fe5920c94..8bd37062d9a6f 100644 --- a/subsys/net/lib/Kconfig +++ b/subsys/net/lib/Kconfig @@ -18,6 +18,8 @@ source "subsys/net/lib/lwm2m/Kconfig" source "subsys/net/lib/sntp/Kconfig" +source "subsys/net/lib/websocket/Kconfig" + endmenu menu "Network Application Support" diff --git a/subsys/net/lib/http/Kconfig b/subsys/net/lib/http/Kconfig index c6b2ff4473880..b408233d448f7 100644 --- a/subsys/net/lib/http/Kconfig +++ b/subsys/net/lib/http/Kconfig @@ -33,9 +33,11 @@ config HTTP_HEADERS int "HTTP header field max number of items" depends on HTTP_SERVER default 8 + default 20 if WEBSOCKET help Number of HTTP header field items that an HTTP server - application will handle + application will handle. If websocket is enabled, then the + default needs to be much bigger. config HTTPS bool "HTTPS support" diff --git a/subsys/net/lib/http/http.c b/subsys/net/lib/http/http.c index e63cf259ae3a6..a73ac5f5ea603 100644 --- a/subsys/net/lib/http/http.c +++ b/subsys/net/lib/http/http.c @@ -66,6 +66,15 @@ int http_close(struct http_ctx *ctx) } #endif +#if defined(CONFIG_HTTP_SERVER) && defined(CONFIG_WEBSOCKET) + if (ctx->websocket.pending) { + net_pkt_unref(ctx->websocket.pending); + ctx->websocket.pending = NULL; + } + + ctx->websocket.data_waiting = 0; +#endif + return net_app_close(&ctx->app_ctx); } diff --git a/subsys/net/lib/http/http_server.c b/subsys/net/lib/http/http_server.c index e167bba1423f2..a175d66491b3c 100644 --- a/subsys/net/lib/http/http_server.c +++ b/subsys/net/lib/http/http_server.c @@ -25,6 +25,11 @@ #include #include +#if defined(CONFIG_WEBSOCKET) +#include +#include "../websocket/websocket_internal.h" +#endif + #define BUF_ALLOC_TIMEOUT 100 #define HTTP_DEFAULT_PORT 80 @@ -314,6 +319,10 @@ static inline void new_client(struct http_ctx *ctx, struct net_context *net_ctx; const char *type_str = "HTTP"; + if (type == WS_CONNECTION) { + type_str = "WS"; + } + net_ctx = get_server_ctx(app_ctx, dst); if (net_ctx) { NET_INFO("[%p] %s connection from %s (%p)", ctx, type_str, @@ -534,6 +543,17 @@ static void http_closed(struct net_app_ctx *app_ctx, if (ctx->cb.close) { ctx->cb.close(ctx, 0, ctx->user_data); } + +#if defined(CONFIG_WEBSOCKET) + if (ctx->websocket.pending) { + net_pkt_unref(ctx->websocket.pending); + ctx->websocket.pending = NULL; + } + + ctx->websocket.data_waiting = 0; +#endif + + ctx->http.field_values_ctr = 0; } static void http_received(struct net_app_ctx *app_ctx, @@ -581,9 +601,9 @@ static void http_received(struct net_app_ctx *app_ctx, if (ctx->state == HTTP_STATE_OPEN) { /* We have active websocket session and there is no longer * any HTTP traffic in the connection. Give the data to - * application to send. + * application. */ - goto http_only; + goto ws_only; } while (frag) { @@ -595,7 +615,7 @@ static void http_received(struct net_app_ctx *app_ctx, ctx->http.request_buf_len) { if (ctx->state == HTTP_STATE_HEADER_RECEIVED) { - goto http_ready; + goto ws_ready; } /* If the caller has not supplied a callback, then @@ -650,7 +670,7 @@ static void http_received(struct net_app_ctx *app_ctx, http_send_error(ctx, 400, NULL, 0, dst); } else { if (ctx->state == HTTP_STATE_HEADER_RECEIVED) { - goto http_ready; + goto ws_ready; } http_process_recv(ctx, dst); @@ -664,17 +684,181 @@ static void http_received(struct net_app_ctx *app_ctx, return; -http_only: +ws_only: if (ctx->cb.recv) { +#if defined(CONFIG_WEBSOCKET) + u32_t msg_len, header_len = 0; + bool masked = true; + int ret; + + if (ctx->websocket.data_waiting == 0) { + ctx->websocket.masking_value = 0; + ctx->websocket.data_read = 0; + ctx->websocket.msg_type_flag = 0; + + if (ctx->websocket.pending) { + /* Append the pending data to current buffer */ + int orig_len; + + orig_len = net_pkt_appdatalen( + ctx->websocket.pending); + net_pkt_set_appdatalen( + ctx->websocket.pending, + orig_len + net_pkt_appdatalen(pkt)); + + net_pkt_frag_add(ctx->websocket.pending, + pkt->frags); + + pkt->frags = NULL; + net_pkt_unref(pkt); + + net_pkt_compact(ctx->websocket.pending); + } else { + ctx->websocket.pending = pkt; + } + + ret = ws_strip_header(ctx->websocket.pending, &masked, + &ctx->websocket.masking_value, + &msg_len, + &ctx->websocket.msg_type_flag, + &header_len); + if (ret < 0) { + /* Not enough bytes for a complete websocket + * header, continue reading data. + */ + NET_DBG("[%p] pending %zd bytes, waiting more", + ctx, + net_pkt_get_len(ctx->websocket.pending)); + return; + } + + if (ctx->websocket.msg_type_flag & WS_FLAG_CLOSE) { + NET_DBG("[%p] Close request from peer", ctx); + http_close(ctx); + return; + } + + if (ctx->websocket.msg_type_flag & WS_FLAG_PING) { + NET_DBG("[%p] Ping request from peer", ctx); + ws_send_msg(ctx, NULL, 0, WS_OPCODE_PONG, + false, true, dst, NULL); + ctx->websocket.data_waiting = 0; + return; + } + + /* We have now received some data. It might not yet be + * the full data that is told by msg_len but we can + * already pass this data to caller. + */ + ctx->websocket.data_waiting = msg_len; + + if (net_pkt_get_len(ctx->websocket.pending) == + header_len) { + NET_DBG("[%p] waiting more data", ctx); + + /* We do not need the websocket header + * any more, so discard it. + */ + net_pkt_unref(ctx->websocket.pending); + ctx->websocket.pending = NULL; + return; + } + + /* If we have more data pending than the header len, + * then discard the header as we do not need that. + */ + net_buf_pull(ctx->websocket.pending->frags, + header_len); + + pkt = ctx->websocket.pending; + ctx->websocket.pending = NULL; + + net_pkt_set_appdatalen(pkt, net_pkt_get_len(pkt)); + net_pkt_set_appdata(pkt, pkt->frags->data); + } + + if (net_pkt_appdatalen(pkt) > ctx->websocket.data_waiting) { + /* Now we received more data which in practice means + * that we got the next websocket header. + */ + struct net_buf *hdr, *payload; + struct net_pkt *cloned; + + ret = net_pkt_split(pkt, pkt->frags, + ctx->websocket.data_waiting, + &payload, &hdr, + ctx->timeout); + if (ret < 0) { + net_pkt_unref(pkt); + return; + } + + net_pkt_frag_unref(pkt->frags); + pkt->frags = NULL; + + cloned = net_pkt_clone(pkt, ctx->timeout); + if (!cloned) { + net_pkt_unref(pkt); + return; + } + + pkt->frags = payload; + payload->frags = NULL; + + net_pkt_compact(pkt); + + cloned->frags = hdr; + + ctx->websocket.pending = cloned; + ctx->websocket.data_waiting = 0; + + net_pkt_set_appdatalen(pkt, net_pkt_get_len(pkt)); + net_pkt_set_appdata(pkt, pkt->frags->data); + + net_pkt_set_appdatalen(cloned, net_pkt_get_len(cloned)); + net_pkt_set_appdata(cloned, cloned->frags->data); + + NET_DBG("More data (%d bytes) received, pending it", + net_pkt_appdatalen(cloned)); + } else { + ctx->websocket.data_waiting -= net_pkt_appdatalen(pkt); + } + + if (ctx->websocket.data_waiting) { + NET_DBG("[%p] waiting still %u bytes", ctx, + ctx->websocket.data_waiting); + } else { + NET_DBG("[%p] All bytes received", ctx); + } + + NET_DBG("[%p] Pass data (%d) to application for processing", + ctx, net_pkt_appdatalen(pkt)); + + NET_DBG("[%p] Masked %s mask 0x%04x", ctx, + masked ? "yes" : "no", + ctx->websocket.masking_value); + + if (masked) { + /* Always deliver unmasked data to the application */ + ws_mask_pkt(pkt, + ctx->websocket.masking_value, + &ctx->websocket.data_read); + } + + ctx->cb.recv(ctx, pkt, 0, ctx->websocket.msg_type_flag, + dst, ctx->user_data); +#else ctx->cb.recv(ctx, pkt, 0, 0, dst, ctx->user_data); +#endif } return; -http_ready: +ws_ready: http_change_state(ctx, HTTP_STATE_OPEN); - url_connected(ctx, HTTP_CONNECT, dst); + url_connected(ctx, WS_CONNECTION, dst); net_pkt_unref(pkt); + ctx->http.field_values_ctr = 0; } #if defined(CONFIG_HTTPS) @@ -786,7 +970,11 @@ static int on_headers_complete(struct http_parser *parser) { ARG_UNUSED(parser); +#if defined(CONFIG_WEBSOCKET) + return ws_headers_complete(parser); +#else return 0; +#endif } static int init_http_parser(struct http_ctx *ctx) diff --git a/subsys/net/lib/websocket/CMakeLists.txt b/subsys/net/lib/websocket/CMakeLists.txt new file mode 100644 index 0000000000000..64d9cf14df364 --- /dev/null +++ b/subsys/net/lib/websocket/CMakeLists.txt @@ -0,0 +1,6 @@ +# base64 support is need from mbedtls +zephyr_include_directories(. $ENV{ZEPHYR_BASE}/ext/lib/crypto/mbedtls/include/) + +zephyr_library() + +zephyr_library_sources_ifdef(CONFIG_WEBSOCKET websocket.c) diff --git a/subsys/net/lib/websocket/Kconfig b/subsys/net/lib/websocket/Kconfig new file mode 100644 index 0000000000000..18b48170d25da --- /dev/null +++ b/subsys/net/lib/websocket/Kconfig @@ -0,0 +1,22 @@ +# Copyright (c) 2017 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 +# + +menuconfig WEBSOCKET + bool "Websocket support [EXPERIMENTAL]" + default n + depends on HTTP + select NET_TCP + help + This option enables the websocket library. + +if WEBSOCKET + +config NET_DEBUG_WEBSOCKET + bool "Debug websocket library" + default n + help + Enables websocket library to output debug messages + +endif # WEBSOCKET diff --git a/subsys/net/lib/websocket/websocket.c b/subsys/net/lib/websocket/websocket.c new file mode 100644 index 0000000000000..ab8c76deaf1c9 --- /dev/null +++ b/subsys/net/lib/websocket/websocket.c @@ -0,0 +1,499 @@ +/* + * Copyright (c) 2017 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#if defined(CONFIG_NET_DEBUG_WEBSOCKET) +#define SYS_LOG_DOMAIN "ws" +#define NET_SYS_LOG_LEVEL SYS_LOG_LEVEL_DEBUG +#define NET_LOG_ENABLED 1 +#endif + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#define BUF_ALLOC_TIMEOUT 100 + +#define HTTP_CRLF "\r\n" + +/* From RFC 6455 chapter 4.2.2 */ +#define WS_MAGIC "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +static void ws_mask_payload(u8_t *payload, size_t payload_len, + u32_t masking_value) +{ + int i; + + for (i = 0; i < payload_len; i++) { + payload[i] ^= masking_value >> (8 * (3 - i % 4)); + } +} + +void ws_mask_pkt(struct net_pkt *pkt, u32_t masking_value, u32_t *data_read) +{ + struct net_buf *frag; + u16_t pos; + int i; + + frag = net_frag_get_pos(pkt, + net_pkt_get_len(pkt) - net_pkt_appdatalen(pkt), + &pos); + if (!frag) { + return; + } + + NET_ASSERT(net_pkt_appdata(pkt) == frag->data + pos); + + while (frag) { + for (i = pos; i < frag->len; i++, (*data_read)++) { + frag->data[i] ^= + masking_value >> (8 * (3 - (*data_read) % 4)); + } + + pos = 0; + frag = frag->frags; + } +} + +int ws_send_msg(struct http_ctx *ctx, u8_t *payload, size_t payload_len, + enum ws_opcode opcode, bool mask, bool final, + const struct sockaddr *dst, + void *user_send_data) +{ + u8_t header[14], hdr_len = 2; + int ret; + + if (ctx->state != HTTP_STATE_OPEN) { + return -ENOTCONN; + } + + if (opcode != WS_OPCODE_DATA_TEXT && opcode != WS_OPCODE_DATA_BINARY && + opcode != WS_OPCODE_CONTINUE && opcode != WS_OPCODE_CLOSE && + opcode != WS_OPCODE_PING && opcode != WS_OPCODE_PONG) { + return -EINVAL; + } + + memset(header, 0, sizeof(header)); + + /* Is this the last packet? */ + header[0] = final ? BIT(7) : 0; + + /* Text, binary, ping, pong or close ? */ + header[0] |= opcode; + + /* Masking */ + header[1] = mask ? BIT(7) : 0; + + if (payload_len < 126) { + header[1] |= payload_len; + } else if (payload_len < 65536) { + header[1] |= 126; + header[2] = payload_len >> 8; + header[3] = payload_len; + hdr_len += 2; + } else { + header[1] |= 127; + header[2] = 0; + header[3] = 0; + header[4] = 0; + header[5] = 0; + header[6] = payload_len >> 24; + header[7] = payload_len >> 16; + header[8] = payload_len >> 8; + header[9] = payload_len; + hdr_len += 8; + } + + /* Add masking value if needed */ + if (mask) { + u32_t masking_value; + + masking_value = sys_rand32_get(); + + header[hdr_len++] |= masking_value >> 24; + header[hdr_len++] |= masking_value >> 16; + header[hdr_len++] |= masking_value >> 8; + header[hdr_len++] |= masking_value; + + ws_mask_payload(payload, payload_len, masking_value); + } + + ret = http_prepare_and_send(ctx, header, hdr_len, dst, user_send_data); + if (ret < 0) { + NET_DBG("Cannot add ws header (%d)", ret); + goto quit; + } + + if (payload) { + ret = http_prepare_and_send(ctx, payload, payload_len, + dst, user_send_data); + if (ret < 0) { + NET_DBG("Cannot send %zd bytes message (%d)", + payload_len, ret); + goto quit; + } + } + + ret = http_send_flush(ctx, user_send_data); + +quit: + return ret; +} + +int ws_strip_header(struct net_pkt *pkt, bool *masked, u32_t *mask_value, + u32_t *message_length, u32_t *message_type_flag, + u32_t *header_len) +{ + struct net_buf *frag; + u16_t value, pos, appdata_pos; + u8_t len; /* message length byte */ + u8_t len_len; /* length of the length field in header */ + + frag = net_frag_read_be16(pkt->frags, 0, &pos, &value); + if (!frag && pos == 0xffff) { + return -ENOMSG; + } + + if (value & 0x8000) { + *message_type_flag |= WS_FLAG_FINAL; + } + + switch (value & 0x0f00) { + case 0x0100: + *message_type_flag |= WS_FLAG_TEXT; + break; + case 0x0200: + *message_type_flag |= WS_FLAG_BINARY; + break; + case 0x0800: + *message_type_flag |= WS_FLAG_CLOSE; + break; + case 0x0900: + *message_type_flag |= WS_FLAG_PING; + break; + case 0x0A00: + *message_type_flag |= WS_FLAG_PONG; + break; + } + + len = value & 0x007f; + if (len < 126) { + len_len = 0; + *message_length = len; + } else if (len == 126) { + u16_t msg_len; + + len_len = 2; + + frag = net_frag_read_be16(frag, pos, &pos, &msg_len); + if (!frag && pos == 0xffff) { + return -ENOMSG; + } + + *message_length = msg_len; + } else { + len_len = 4; + + frag = net_frag_read_be32(frag, pos, &pos, message_length); + if (!frag && pos == 0xffff) { + return -ENOMSG; + } + } + + if (value & 0x0080) { + *masked = true; + appdata_pos = 0; + + frag = net_frag_read_be32(frag, pos, &pos, mask_value); + if (!frag && pos == 0xffff) { + return -ENOMSG; + } + } else { + *masked = false; + appdata_pos = len_len; + } + + frag = net_frag_get_pos(pkt, pos + appdata_pos, &pos); + if (!frag && pos == 0xffff) { + return -ENOMSG; + } + + /* Minimum websocket header is 6 bytes, header length might be + * bigger depending on length field len. + */ + *header_len = 6 + len_len; + + return 0; +} + +static bool field_contains(const char *field, int field_len, + const char *str, int str_len) +{ + bool found = false; + char c, skip; + + c = *str++; + if (c == '\0') { + return false; + } + + str_len--; + + do { + do { + skip = *field++; + field_len--; + if (skip == '\0' || field_len == 0) { + return false; + } + } while (skip != c); + + if (field_len < str_len) { + return false; + } + + if (strncasecmp(field, str, str_len) == 0) { + found = true; + break; + } + + } while (field_len >= str_len); + + return found; +} + +static bool check_ws_headers(struct http_ctx *ctx, struct http_parser *parser, + int *ws_sec_key, int *host, int *subprotocol) +{ + int i, count, connection = -1; + int ws_sec_version = -1; + + if (!parser->upgrade || parser->method != HTTP_GET || + parser->http_major != 1 || parser->http_minor != 1) { + return false; + } + + for (i = 0, count = 0; i < ctx->http.field_values_ctr; i++) { + if (ctx->http.field_values[i].key_len == 0) { + continue; + } + + if (strncasecmp(ctx->http.field_values[i].key, + "Sec-WebSocket-Key", + sizeof("Sec-WebSocket-Key") - 1) == 0) { + *ws_sec_key = i; + continue; + } + + if (strncasecmp(ctx->http.field_values[i].key, + "Sec-WebSocket-Version", + sizeof("Sec-WebSocket-Version") - 1) == 0) { + if (strncmp(ctx->http.field_values[i].value, + "13", sizeof("13") - 1) == 0) { + ws_sec_version = i; + } + + continue; + } + + if (strncasecmp(ctx->http.field_values[i].key, + "Connection", sizeof("Connection") - 1) == 0) { + if (field_contains( + ctx->http.field_values[i].value, + ctx->http.field_values[i].value_len, + "Upgrade", sizeof("Upgrade") - 1)) { + connection = i; + } + + continue; + } + + if (strncasecmp(ctx->http.field_values[i].key, "Host", + sizeof("Host") - 1) == 0) { + *host = i; + continue; + } + + if (strncasecmp(ctx->http.field_values[i].key, + "Sec-WebSocket-Protocol", + sizeof("Sec-WebSocket-Protocol") - 1) == 0) { + *subprotocol = i; + continue; + } + } + + if (connection >= 0 && *ws_sec_key >= 0 && ws_sec_version >= 0 && + *host >= 0) { + return true; + } + + return false; +} + +static struct net_pkt *prepare_reply(struct http_ctx *ctx, + int ws_sec_key, int host, int subprotocol) +{ + char key_accept[32 + sizeof(WS_MAGIC) - 1]; + char accept[20]; + struct net_pkt *pkt; + char tmp[64]; + int ret; + size_t olen; + + pkt = net_app_get_net_pkt_with_dst(&ctx->app_ctx, + ctx->http.parser.addr, + ctx->timeout); + if (!pkt) { + return NULL; + } + + snprintk(tmp, sizeof(tmp), "HTTP/1.1 101 OK\r\n"); + if (!net_pkt_append_all(pkt, strlen(tmp), (u8_t *)tmp, ctx->timeout)) { + goto fail; + } + + snprintk(tmp, sizeof(tmp), "User-Agent: %s\r\n", ZEPHYR_USER_AGENT); + if (!net_pkt_append_all(pkt, strlen(tmp), (u8_t *)tmp, ctx->timeout)) { + goto fail; + } + + snprintk(tmp, sizeof(tmp), "Upgrade: websocket\r\n"); + if (!net_pkt_append_all(pkt, strlen(tmp), (u8_t *)tmp, ctx->timeout)) { + goto fail; + } + + snprintk(tmp, sizeof(tmp), "Connection: Upgrade\r\n"); + if (!net_pkt_append_all(pkt, strlen(tmp), (u8_t *)tmp, ctx->timeout)) { + goto fail; + } + + olen = min(sizeof(key_accept), + ctx->http.field_values[ws_sec_key].value_len); + strncpy(key_accept, ctx->http.field_values[ws_sec_key].value, olen); + + olen = min(sizeof(key_accept) - + ctx->http.field_values[ws_sec_key].value_len, + sizeof(WS_MAGIC) - 1); + strncpy(key_accept + ctx->http.field_values[ws_sec_key].value_len, + WS_MAGIC, olen); + + olen = ctx->http.field_values[ws_sec_key].value_len + + sizeof(WS_MAGIC) - 1; + + mbedtls_sha1(key_accept, olen, accept); + + snprintk(tmp, sizeof(tmp), "Sec-WebSocket-Accept: "); + + ret = mbedtls_base64_encode(tmp + sizeof("Sec-WebSocket-Accept: ") - 1, + sizeof(tmp) - + (sizeof("Sec-WebSocket-Accept: ") - 1), + &olen, accept, sizeof(accept)); + if (ret) { + if (ret == MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) { + NET_DBG("[%p] Too short buffer olen %zd", ctx, olen); + } + + goto fail; + } + + snprintk(tmp + sizeof("Sec-WebSocket-Accept: ") - 1 + olen, + sizeof(tmp) - (sizeof("Sec-WebSocket-Accept: ") - 1) - olen, + "\r\n\r\n"); + + if (!net_pkt_append_all(pkt, strlen(tmp), (u8_t *)tmp, ctx->timeout)) { + goto fail; + } + + return pkt; + +fail: + net_pkt_unref(pkt); + return NULL; +} + +int ws_headers_complete(struct http_parser *parser) +{ + struct http_ctx *ctx = parser->data; + int ws_sec_key = -1, host = -1, subprotocol = -1; + + if (check_ws_headers(ctx, parser, &ws_sec_key, &host, + &subprotocol)) { + struct net_pkt *pkt; + struct http_root_url *url; + int ret; + + url = http_url_find(ctx, HTTP_URL_WEBSOCKET); + if (!url) { + url = http_url_find(ctx, HTTP_URL_STANDARD); + if (url) { + /* Normal HTTP URL was found */ + return 0; + } + + /* If there is no URL to serve this websocket + * request, then just bail out. + */ + if (!ctx->http.urls) { + NET_DBG("[%p] No URL handlers found", ctx); + return 0; + } + + url = &ctx->http.urls->default_url; + if (url && url->is_used && + ctx->http.urls->default_cb) { + ret = ctx->http.urls->default_cb(ctx, + WS_CONNECTION, + ctx->http.parser.addr); + if (ret == HTTP_VERDICT_ACCEPT) { + goto accept; + } + } + + if (url->flags == HTTP_URL_WEBSOCKET) { + goto fail; + } + } + + if (url->flags != HTTP_URL_WEBSOCKET) { + return 0; + } + +accept: + NET_DBG("[%p] ws header %d fields found", ctx, + ctx->http.field_values_ctr + 1); + + pkt = prepare_reply(ctx, ws_sec_key, host, subprotocol); + if (!pkt) { + goto fail; + } + + net_pkt_set_appdatalen(pkt, net_buf_frags_len(pkt->frags)); + + ret = net_app_send_pkt(&ctx->app_ctx, pkt, NULL, 0, 0, + INT_TO_POINTER(K_FOREVER)); + if (ret) { + goto fail; + } + + http_change_state(ctx, HTTP_STATE_HEADER_RECEIVED); + + /* We do not expect any HTTP data after this */ + return 2; + +fail: + http_change_state(ctx, HTTP_STATE_CLOSED); + } + + return 0; +} diff --git a/subsys/net/lib/websocket/websocket_internal.h b/subsys/net/lib/websocket/websocket_internal.h new file mode 100644 index 0000000000000..b4ecb069f71a5 --- /dev/null +++ b/subsys/net/lib/websocket/websocket_internal.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2017 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __WEBSOCKET_INTERNAL_H__ +#define __WEBSOCKET_INTERNAL_H__ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Strip websocket header from the packet. + * + * @details The function will remove websocket header from the network packet. + * + * @param pkt Received network packet + * @param masked The mask status of the message is returned. + * @param mask_value The mask value of the message is returned. + * @param message_length Total length of the message from websocket header. + * @param message_type_flag Type of the websocket message (WS_FLAG_xxx value) + * @param header_len Length of the websocket header is returned to caller. + * + * @return 0 if ok, <0 if error + */ +int ws_strip_header(struct net_pkt *pkt, bool *masked, u32_t *mask_value, + u32_t *message_length, u32_t *message_type_flag, + u32_t *header_len); + +/** + * @brief Mask or unmask a websocket message if needed + * + * @details The function will either add or remove the masking from the data. + * + * @param pkt Network packet to process + * @param masking_value The mask value to use. + * @param data_read How many bytes we have read. This is modified by this + * function. + */ +void ws_mask_pkt(struct net_pkt *pkt, u32_t masking_value, u32_t *data_read); + +/** + * @brief This is called by HTTP server after all the HTTP headers have been + * received. + * + * @details The function will check if this is a valid websocket connection + * or not. + * + * @param parser HTTP parser instance + * + * @return 0 if ok, 1 if there is no body, 2 if HTTP connection is to be + * upgraded to websocket one + */ +int ws_headers_complete(struct http_parser *parser); + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* __WS_H__ */ diff --git a/subsys/shell/shell.c b/subsys/shell/shell.c index c0934e422dc1b..f4f8789f80342 100644 --- a/subsys/shell/shell.c +++ b/subsys/shell/shell.c @@ -28,6 +28,9 @@ #ifdef CONFIG_NATIVE_POSIX_CONSOLE #include #endif +#ifdef CONFIG_WEBSOCKET_CONSOLE +#include +#endif #include @@ -638,6 +641,9 @@ void shell_init(const char *str) #ifdef CONFIG_NATIVE_POSIX_STDIN_CONSOLE native_stdin_register_input(&avail_queue, &cmds_queue, completion); #endif +#ifdef CONFIG_WEBSOCKET_CONSOLE + ws_register_input(&avail_queue, &cmds_queue, completion); +#endif } /** @brief Optionally register an app default cmd handler. diff --git a/tests/net/websocket/CMakeLists.txt b/tests/net/websocket/CMakeLists.txt new file mode 100644 index 0000000000000..6eba191b8017b --- /dev/null +++ b/tests/net/websocket/CMakeLists.txt @@ -0,0 +1,21 @@ +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(NONE) + +target_include_directories(app PRIVATE $ENV{ZEPHYR_BASE}/subsys/net/ip) +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) + +# List of files that are used to generate .h file that can be included +# into .c file. +foreach(inc_file + echo-apps-cert.der + echo-apps-key.der + ) + generate_inc_file_for_target( + app + src/${inc_file} + ${ZEPHYR_BINARY_DIR}/include/generated/${inc_file}.inc + ) +endforeach() + +target_link_libraries_ifdef(CONFIG_MBEDTLS app mbedTLS) diff --git a/tests/net/websocket/prj.conf b/tests/net/websocket/prj.conf new file mode 100644 index 0000000000000..6824795333d75 --- /dev/null +++ b/tests/net/websocket/prj.conf @@ -0,0 +1,74 @@ +# Testing infra +CONFIG_ZTEST=y +CONFIG_ZTEST_STACKSIZE=2048 +CONFIG_NET_TEST=y +CONFIG_NET_LOOPBACK=y +CONFIG_MAIN_STACK_SIZE=2048 + +# Generic IP stack options and features +CONFIG_NETWORKING=y +CONFIG_NET_UDP=n +CONFIG_NET_TCP=y +CONFIG_NET_IPV6=y +CONFIG_NET_IPV4=y +#CONFIG_NET_DHCPV4=y +CONFIG_ENTROPY_GENERATOR=y +CONFIG_TEST_RANDOM_GENERATOR=y +CONFIG_INIT_STACKS=y +CONFIG_NET_MAX_CONTEXTS=8 +CONFIG_NET_SHELL=y +CONFIG_NET_IPV6_ND=n +CONFIG_NET_IPV6_MLD=n +CONFIG_NET_IPV6_DAD=n + +# Number of network buffers +CONFIG_NET_PKT_RX_COUNT=32 +CONFIG_NET_PKT_TX_COUNT=32 +CONFIG_NET_BUF_RX_COUNT=32 +CONFIG_NET_BUF_TX_COUNT=32 +CONFIG_NET_CONTEXT_NET_PKT_POOL=y + +# IPv6 address counts +CONFIG_NET_IF_UNICAST_IPV6_ADDR_COUNT=3 +CONFIG_NET_IF_MCAST_IPV6_ADDR_COUNT=4 + +# Network application settings +CONFIG_NET_APP=y +CONFIG_NET_APP_SETTINGS=y +CONFIG_NET_APP_NEED_IPV6=y +CONFIG_NET_APP_NEED_IPV4=y +CONFIG_NET_APP_MY_IPV6_ADDR="2001:db8::1" +CONFIG_NET_APP_MY_IPV4_ADDR="192.0.2.1" +CONFIG_NET_APP_INIT_TIMEOUT=1 +CONFIG_NET_APP_CLIENT=y + +# HTTP & Websocket options +CONFIG_WEBSOCKET=y +CONFIG_HTTP=y +CONFIG_HTTPS=n +CONFIG_HTTP_SERVER=y +# How many URLs we are serving +CONFIG_HTTP_SERVER_NUM_URLS=5 + +# base64 support needed by websocket +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_CFG_FILE="config-mini-tls1_2.h" + +# Logging +CONFIG_NET_LOG=y +CONFIG_SYS_LOG_NET_LEVEL=2 +CONFIG_SYS_LOG_SHOW_COLOR=y +CONFIG_NET_STATISTICS=y + +# Debugging +CONFIG_NET_DEBUG_WEBSOCKET=n +CONFIG_NET_DEBUG_HTTP=n +CONFIG_NET_DEBUG_APP=n +CONFIG_NET_DEBUG_NET_PKT=n +CONFIG_NET_DEBUG_CORE=n +CONFIG_NET_DEBUG_CONTEXT=n +CONFIG_NET_DEBUG_CONN=n +CONFIG_NET_DEBUG_TCP=n +CONFIG_NET_DEBUG_IF=n +CONFIG_NET_DEBUG_IPV6=n diff --git a/tests/net/websocket/src/echo-apps-cert.der b/tests/net/websocket/src/echo-apps-cert.der new file mode 100644 index 0000000000000..bfcb335e31c8c Binary files /dev/null and b/tests/net/websocket/src/echo-apps-cert.der differ diff --git a/tests/net/websocket/src/echo-apps-key.der b/tests/net/websocket/src/echo-apps-key.der new file mode 100644 index 0000000000000..5a4d67372ea41 Binary files /dev/null and b/tests/net/websocket/src/echo-apps-key.der differ diff --git a/tests/net/websocket/src/main.c b/tests/net/websocket/src/main.c new file mode 100644 index 0000000000000..135c1a175aa89 --- /dev/null +++ b/tests/net/websocket/src/main.c @@ -0,0 +1,604 @@ +/* main.c - Application main entry point */ + +/* + * Copyright (c) 2017 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* In this websocket test, we create a websocket server which starts + * to listen connections. Then we start to send data to it and verify that + * we get proper data back. + */ + +#include + +#include +#include +#include + +static struct net_app_ctx app_ctx_v6; +static struct net_app_ctx app_ctx_v4; + +#if defined(CONFIG_NET_DEBUG_WEBSOCKET) +#define DBG(fmt, ...) printk(fmt, ##__VA_ARGS__) +#define NET_LOG_ENABLED 1 +#else +#define DBG(fmt, ...) +#endif + +#include "../../../subsys/net/ip/net_private.h" + +/* + * GET /ws HTTP/1.1 + * Upgrade: websocket + * Connection: Upgrade + * Host: 2001:db8::1 + * Origin: http://2001:db8::1 + * Sec-WebSocket-Key: 8VMFeU0j8bImbjyjPVHSQg== + * Sec-WebSocket-Version: 13 + */ +static char http_msg[] = { + 0x47, 0x45, 0x54, 0x20, 0x2f, 0x77, 0x73, 0x20, + 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, 0x2e, 0x31, + 0x0d, 0x0a, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, + 0x65, 0x3a, 0x20, 0x77, 0x65, 0x62, 0x73, 0x6f, + 0x63, 0x6b, 0x65, 0x74, 0x0d, 0x0a, 0x43, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x3a, 0x20, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, + 0x65, 0x0d, 0x0a, 0x48, 0x6f, 0x73, 0x74, 0x3a, + 0x20, 0x32, 0x30, 0x30, 0x31, 0x3a, 0x64, 0x62, + 0x38, 0x3a, 0x3a, 0x31, 0x0d, 0x0a, 0x4f, 0x72, + 0x69, 0x67, 0x69, 0x6e, 0x3a, 0x20, 0x68, 0x74, + 0x74, 0x70, 0x3a, 0x2f, 0x2f, 0x32, 0x30, 0x30, + 0x31, 0x3a, 0x64, 0x62, 0x38, 0x3a, 0x3a, 0x31, + 0x0d, 0x0a, 0x53, 0x65, 0x63, 0x2d, 0x57, 0x65, + 0x62, 0x53, 0x6f, 0x63, 0x6b, 0x65, 0x74, 0x2d, + 0x4b, 0x65, 0x79, 0x3a, 0x20, 0x38, 0x56, 0x4d, + 0x46, 0x65, 0x55, 0x30, 0x6a, 0x38, 0x62, 0x49, + 0x6d, 0x62, 0x6a, 0x79, 0x6a, 0x50, 0x56, 0x48, + 0x53, 0x51, 0x67, 0x3d, 0x3d, 0x0d, 0x0a, 0x53, + 0x65, 0x63, 0x2d, 0x57, 0x65, 0x62, 0x53, 0x6f, + 0x63, 0x6b, 0x65, 0x74, 0x2d, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x3a, 0x20, 0x31, 0x33, + 0x0d, 0x0a, 0x0d, 0x0a, +}; + +/* WebSocket: + * FIN: true + * Reserved: 0x00 + * Opcode: Text (1) + * Mask: True + * Payload len: 7 + * Masking key: d1ffa558 + * Masked payload: 0x99, 0x9a, 0xc9, 0x34, 0xbe, 0xd3, 0x85 + * Payload: "Hello, " + */ +/* This array is not modified */ +static const u8_t ws_test_msg_orig[] = { + 0x81, 0x87, 0xd1, 0xff, 0xa5, 0x58, 0x99, 0x9a, + 0xc9, 0x34, 0xbe, 0xd3, 0x85, +}; + +/* We are manipulating this array in the test */ +static u8_t ws_test_msg[sizeof(ws_test_msg_orig)]; + +static u32_t mask_value = 0xd1ffa558; +static struct sockaddr server_addr; +static s32_t timeout = K_MSEC(100); +static int bytes_received; +static bool failure; +static struct k_sem wait_data; +static struct k_sem progress; +extern struct http_ctx *ws_ctx; + +#define hdr_len 6 + +static u8_t ws_unmasked_msg[sizeof(ws_test_msg) - hdr_len]; +static int total_data_len = sizeof(ws_unmasked_msg); + +#define WAIT_TIME K_SECONDS(1) + +void test_websocket_init_server(void); +void test_websocket_cleanup_server(void); + +static void ws_mask_payload(u8_t *payload, size_t payload_len, + u32_t masking_value) +{ + int i; + + for (i = 0; i < payload_len; i++) { + payload[i] ^= masking_value >> (8 * (3 - i % 4)); + } +} + +static void recv_cb(struct net_app_ctx *ctx, + struct net_pkt *pkt, + int status, + void *user_data) +{ + int ret; + size_t len; + + if (!pkt) { + return; + } + + len = net_pkt_appdatalen(pkt); + + /* The pkt can contain websocket header because we are bypassing + * any websocket message parsing here. So we need to skip the websocket + * header here in that case. The return header length is 2 bytes here + * only if the returned message < 127 bytes long. + */ + if (net_pkt_appdata(pkt)[0] == 0x01) { + /* If we received packet with websocket header, then skip it */ + net_buf_pull(pkt->frags, 2); + net_pkt_set_appdata(pkt, net_pkt_appdata(pkt) + 2); + + len -= 2; + net_pkt_set_appdatalen(pkt, len); + } + + if (len == 0) { + goto out; + } + + DBG("Received %zd bytes\n", len); + + /* Note that we can only use net_pkt_appdata() here because we + * know that the data fits in first fragment. + */ + ret = memcmp(ws_unmasked_msg + bytes_received, + net_pkt_appdata(pkt), len); + if (ret != 0) { + net_hexdump("recv", net_pkt_appdata(pkt), + net_pkt_appdatalen(pkt)); + net_hexdump("sent", ws_unmasked_msg, sizeof(ws_unmasked_msg)); + + failure = true; + + zassert_equal(ret, 0, "Received data does not match"); + + goto out; + } + + bytes_received += len; + failure = false; + + if (total_data_len == bytes_received) { + bytes_received = 0; + } + +out: + k_sem_give(&wait_data); + k_sem_give(&progress); + + net_pkt_unref(pkt); +} + +void test_init(void) +{ + /* The semaphore is there to wait the data to be received. */ + k_sem_init(&wait_data, 0, UINT_MAX); + + k_sem_init(&progress, 0, UINT_MAX); + + memcpy(ws_unmasked_msg, ws_test_msg_orig + hdr_len, + sizeof(ws_unmasked_msg)); + + ws_mask_payload(ws_unmasked_msg, sizeof(ws_unmasked_msg), mask_value); +} + +static void test_connect(struct net_app_ctx *ctx) +{ + int ret; + + ret = net_app_connect(ctx, K_FOREVER); + zassert_equal(ret, 0, "websocket client connect"); +} + +static void test_close(struct net_app_ctx *ctx) +{ + int ret; + + ret = net_app_close(ctx); + zassert_equal(ret, 0, "websocket client close"); +} + +/* The chunk_size tells how many bytes at a time to send. + * This is not the same as HTTP chunk! + */ +static void test_send_recv(int chunk_size, struct net_app_ctx *ctx) +{ + int i, j, ret; + + DBG("Sending %d bytes at a time\n", chunk_size); + + for (i = 0; i < sizeof(ws_test_msg); i += chunk_size) { + for (j = 0; + IS_ENABLED(CONFIG_NET_DEBUG_WEBSOCKET) && j < chunk_size; + j++) { + if ((i + chunk_size) >= sizeof(ws_test_msg)) { + break; + } + + if ((i + j) < hdr_len) { + DBG("[%d] = 0x%02x\n", i + j, + ws_test_msg[i + j]); + } else { + DBG("[%d] = 0x%02x -> \"%c\"\n", i + j, + ws_test_msg[i + j], + ws_unmasked_msg[i + j - hdr_len]); + } + } + + if ((i + chunk_size) >= sizeof(ws_test_msg)) { + chunk_size = sizeof(ws_test_msg) - i; + } + + ret = net_app_send_buf(ctx, &ws_test_msg[i], + chunk_size, + &server_addr, + sizeof(struct sockaddr), + timeout, + NULL); + if (ret != 0) { + DBG("Cannot send %d byte(s) (%d)\n", ret, chunk_size); + + zassert_equal(ret, 0, "websocket IPv6 client ws send"); + } + + /* Make sure the receiving side gets the data now */ + k_yield(); + } +} + +static void test_send_multi_msg(struct net_app_ctx *ctx) +{ + u8_t ws_big_msg[sizeof(ws_test_msg) * 2]; + int i, j, ret, chunk_size = 4; + + k_sem_take(&progress, K_FOREVER); + + memcpy(ws_test_msg, ws_test_msg_orig, sizeof(ws_test_msg)); + bytes_received = 0; + + ws_ctx->websocket.data_waiting = 0; + if (ws_ctx->websocket.pending) { + net_pkt_unref(ws_ctx->websocket.pending); + ws_ctx->websocket.pending = NULL; + } + + memcpy(ws_big_msg, ws_test_msg, sizeof(ws_test_msg)); + memcpy(ws_big_msg + sizeof(ws_test_msg), ws_test_msg, + sizeof(ws_test_msg)); + + for (i = 0; i < sizeof(ws_big_msg); i += chunk_size) { + for (j = 0; + IS_ENABLED(CONFIG_NET_DEBUG_WEBSOCKET) && j < chunk_size; + j++) { + int first_msg = 0; + + if ((i + chunk_size) >= sizeof(ws_big_msg)) { + break; + } + + if (i > sizeof(ws_test_msg)) { + first_msg = sizeof(ws_test_msg); + } + + if (i + j + first_msg < hdr_len + first_msg) { + DBG("[%d] = 0x%02x\n", i + j, + ws_big_msg[i + j + first_msg]); + } else { + if (i + j + first_msg < + sizeof(ws_test_msg) + first_msg) { + DBG("[%d] = 0x%02x -> \"%c\"\n", i + j, + ws_big_msg[i + j + first_msg], + ws_unmasked_msg[i + j - hdr_len + + first_msg]); + } + } + } + + if ((i + chunk_size) >= sizeof(ws_big_msg)) { + chunk_size = sizeof(ws_big_msg) - i; + } + + ret = net_app_send_buf(ctx, &ws_big_msg[i], + chunk_size, + &server_addr, + sizeof(struct sockaddr), + timeout, + NULL); + if (ret != 0) { + DBG("Cannot send %d byte(s) (%d)\n", ret, chunk_size); + + zassert_equal(ret, 0, "websocket client ws send"); + } + + /* Make sure the receiving side gets the data now */ + k_yield(); + } +} + +/* Start to send raw data and do not use websocket client API for this so + * that we can send garbage data if needed. + */ +void test_v6_init(void) +{ + int ret; + + ret = net_ipaddr_parse(CONFIG_NET_APP_MY_IPV6_ADDR, + strlen(CONFIG_NET_APP_MY_IPV6_ADDR), + &server_addr); + zassert_equal(ret, 1, "cannot parse server address"); + + ret = net_app_init_tcp_client(&app_ctx_v6, + NULL, + NULL, + CONFIG_NET_APP_MY_IPV6_ADDR, + 80, + 0, + NULL); + zassert_equal(ret, 0, "websocket IPv6 client init"); + + net_app_set_cb(&app_ctx_v6, NULL, recv_cb, NULL, NULL); +} + +void test_v6_connect(void) +{ + test_connect(&app_ctx_v6); + + k_sem_give(&progress); +} + +void test_v6_close(void) +{ + test_close(&app_ctx_v6); +} + +static void test_v6_send_recv(int chunk_size) +{ + static int header_sent; + int ret; + + if (!header_sent) { + ret = net_app_send_buf(&app_ctx_v6, http_msg, + sizeof(http_msg) - 1, + &server_addr, + sizeof(struct sockaddr_in6), + timeout, NULL); + if (ret != 0) { + DBG("Cannot send byte (%d)\n", ret); + + zassert_equal(ret, 0, + "websocket IPv6 client http send"); + } + + header_sent = true; + } + + test_send_recv(chunk_size, &app_ctx_v6); +} + +void test_v6_send_recv_n(int chunk_size) +{ + k_sem_take(&progress, K_FOREVER); + + /* Make sure we have a fresh start before running this specific test */ + memcpy(ws_test_msg, ws_test_msg_orig, sizeof(ws_test_msg)); + bytes_received = 0; + + ws_ctx->websocket.data_waiting = 0; + if (ws_ctx->websocket.pending) { + net_pkt_unref(ws_ctx->websocket.pending); + ws_ctx->websocket.pending = NULL; + } + + test_v6_send_recv(chunk_size); + + if (k_sem_take(&wait_data, WAIT_TIME)) { + zassert_true(false, "Timeout while waiting data"); + } + + zassert_false(failure, "Send test failed"); +} + +void test_v6_send_recv_1(void) +{ + test_v6_send_recv_n(1); +} + +void test_v6_send_recv_2(void) +{ + test_v6_send_recv_n(2); +} + +void test_v6_send_recv_3(void) +{ + test_v6_send_recv_n(3); +} + +void test_v6_send_recv_4(void) +{ + test_v6_send_recv_n(4); +} + +void test_v6_send_recv_5(void) +{ + test_v6_send_recv_n(5); +} + +void test_v6_send_recv_6(void) +{ + test_v6_send_recv_n(6); +} + +void test_v6_send_recv_7(void) +{ + test_v6_send_recv_n(7); +} + +void test_v6_send_multi_msg(void) +{ + test_send_multi_msg(&app_ctx_v6); +} + +/* Start to send raw data and do not use websocket client API for this so + * that we can send garbage data if needed. + */ +void test_v4_init(void) +{ + int ret; + + ret = net_ipaddr_parse(CONFIG_NET_APP_MY_IPV4_ADDR, + strlen(CONFIG_NET_APP_MY_IPV4_ADDR), + &server_addr); + zassert_equal(ret, 1, "cannot parse server address"); + + ret = net_app_init_tcp_client(&app_ctx_v4, + NULL, + NULL, + CONFIG_NET_APP_MY_IPV4_ADDR, + 80, + 0, + NULL); + zassert_equal(ret, 0, "websocket IPv4 client init"); + + net_app_set_cb(&app_ctx_v4, NULL, recv_cb, NULL, NULL); +} + +void test_v4_connect(void) +{ + test_connect(&app_ctx_v4); + + k_sem_give(&progress); +} + +void test_v4_close(void) +{ + test_close(&app_ctx_v4); +} + +static void test_v4_send_recv(int chunk_size) +{ + static int header_sent; + int ret; + + if (!header_sent) { + ret = net_app_send_buf(&app_ctx_v4, http_msg, + sizeof(http_msg) - 1, + &server_addr, + sizeof(struct sockaddr_in), + timeout, NULL); + if (ret != 0) { + DBG("Cannot send byte (%d)\n", ret); + + zassert_equal(ret, 0, + "websocket IPv4 client http send"); + } + + header_sent = true; + } + + test_send_recv(chunk_size, &app_ctx_v4); +} + +void test_v4_send_recv_n(int chunk_size) +{ + k_sem_take(&progress, K_FOREVER); + + /* Make sure we have a fresh start before running this specific test */ + memcpy(ws_test_msg, ws_test_msg_orig, sizeof(ws_test_msg)); + bytes_received = 0; + + ws_ctx->websocket.data_waiting = 0; + if (ws_ctx->websocket.pending) { + net_pkt_unref(ws_ctx->websocket.pending); + ws_ctx->websocket.pending = NULL; + } + + test_v4_send_recv(chunk_size); + + if (k_sem_take(&wait_data, WAIT_TIME)) { + zassert_true(false, "Timeout while waiting data"); + } + + zassert_false(failure, "Send test failed"); +} + +void test_v4_send_recv_1(void) +{ + test_v4_send_recv_n(1); +} + +void test_v4_send_recv_2(void) +{ + test_v4_send_recv_n(2); +} + +void test_v4_send_recv_3(void) +{ + test_v4_send_recv_n(3); +} + +void test_v4_send_recv_4(void) +{ + test_v4_send_recv_n(4); +} + +void test_v4_send_recv_5(void) +{ + test_v4_send_recv_n(5); +} + +void test_v4_send_recv_6(void) +{ + test_v4_send_recv_n(6); +} + +void test_v4_send_recv_7(void) +{ + test_v4_send_recv_n(7); +} + +void test_v4_send_multi_msg(void) +{ + test_send_multi_msg(&app_ctx_v4); +} + +void test_main(void) +{ + ztest_test_suite(websocket, + ztest_unit_test(test_websocket_init_server), + ztest_unit_test(test_init), + ztest_unit_test(test_v6_init), + ztest_unit_test(test_v6_connect), + ztest_unit_test(test_v6_send_recv_1), + ztest_unit_test(test_v6_send_recv_2), + ztest_unit_test(test_v6_send_recv_3), + ztest_unit_test(test_v6_send_recv_4), + ztest_unit_test(test_v6_send_recv_5), + ztest_unit_test(test_v6_send_recv_6), + ztest_unit_test(test_v6_send_recv_7), + ztest_unit_test(test_v6_send_multi_msg), + ztest_unit_test(test_v6_close), + ztest_unit_test(test_websocket_cleanup_server), + ztest_unit_test(test_websocket_init_server), + ztest_unit_test(test_v4_init), + ztest_unit_test(test_v4_connect), + ztest_unit_test(test_v4_send_recv_1), + ztest_unit_test(test_v4_send_recv_2), + ztest_unit_test(test_v4_send_recv_3), + ztest_unit_test(test_v4_send_recv_4), + ztest_unit_test(test_v4_send_recv_5), + ztest_unit_test(test_v4_send_recv_6), + ztest_unit_test(test_v4_send_recv_7), + ztest_unit_test(test_v4_send_multi_msg), + ztest_unit_test(test_v4_close), + ztest_unit_test(test_websocket_cleanup_server)); + + ztest_run_test_suite(websocket); +} diff --git a/tests/net/websocket/src/server.c b/tests/net/websocket/src/server.c new file mode 100644 index 0000000000000..b1612fc48c86a --- /dev/null +++ b/tests/net/websocket/src/server.c @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2017 Intel Corporation. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#if defined(CONFIG_NET_DEBUG_WEBSOCKET) +#define SYS_LOG_DOMAIN "test-ws-server" +#define NET_SYS_LOG_LEVEL SYS_LOG_LEVEL_DEBUG +#define NET_LOG_ENABLED 1 +#endif + +#include +#include +#include + +#include +#include +#include + +#include + +#include "../../../subsys/net/ip/net_private.h" + +#define MAX_BUF_LEN 128 +#define MAX_URL_LEN 128 +#define SEND_TIMEOUT K_SECONDS(10) +#define ALLOC_TIMEOUT 100 + +struct http_ctx *ws_ctx; +static struct http_ctx http_ctx; +static struct http_server_urls http_urls; + +/* Note that both tcp and udp can share the same pool but in this + * example the UDP context and TCP context have separate pools. + */ +#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL) +NET_PKT_TX_SLAB_DEFINE(echo_tx_tcp, 15); +NET_PKT_DATA_POOL_DEFINE(echo_data_tcp, 30); + +static struct k_mem_slab *tx_tcp_slab(void) +{ + return &echo_tx_tcp; +} + +static struct net_buf_pool *data_tcp_pool(void) +{ + return &echo_data_tcp; +} +#else +#define tx_tcp_slab NULL +#define data_tcp_pool NULL +#endif /* CONFIG_NET_CONTEXT_NET_PKT_POOL */ + +/* The result buf size is set to large enough so that we can receive max size + * buf back. Note that mbedtls needs also be configured to have equal size + * value for its buffer size. See MBEDTLS_SSL_MAX_CONTENT_LEN option in TLS + * config file. + */ +#define RESULT_BUF_SIZE 1500 +static u8_t result[RESULT_BUF_SIZE]; + +#if defined(CONFIG_HTTPS) + +#if !defined(CONFIG_NET_APP_TLS_STACK_SIZE) +#define CONFIG_NET_APP_TLS_STACK_SIZE 8192 +#endif /* CONFIG_NET_APP_TLS_STACK_SIZE */ + +#define APP_BANNER "Run TLS ws-server" +#define INSTANCE_INFO "Zephyr TLS ws-server #1" + +/* Note that each net_app context needs its own stack as there will be + * a separate thread needed. + */ +NET_STACK_DEFINE(WS_ECHO_SERVER, ws_tls_stack, + CONFIG_NET_APP_TLS_STACK_SIZE, CONFIG_NET_APP_TLS_STACK_SIZE); + +#define RX_FIFO_DEPTH 4 +K_MEM_POOL_DEFINE(ssl_pool, 4, 64, RX_FIFO_DEPTH, 4); +#endif /* CONFIG_HTTPS */ + +#if defined(CONFIG_HTTPS) +/* Load the certificates and private RSA key. */ + +static const char echo_apps_cert_der[] = { +#include "echo-apps-cert.der.inc" +}; + +static const char echo_apps_key_der[] = { +#include "echo-apps-key.der.inc" +}; + +static int setup_cert(struct net_app_ctx *ctx, + mbedtls_x509_crt *cert, + mbedtls_pk_context *pkey) +{ + int ret; + + ret = mbedtls_x509_crt_parse(cert, echo_apps_cert_der, + sizeof(echo_apps_cert_der)); + if (ret != 0) { + NET_ERR("mbedtls_x509_crt_parse returned %d", ret); + return ret; + } + + ret = mbedtls_pk_parse_key(pkey, echo_apps_key_der, + sizeof(echo_apps_key_der), NULL, 0); + if (ret != 0) { + NET_ERR("mbedtls_pk_parse_key returned %d", ret); + return ret; + } + + return 0; +} +#endif /* CONFIG_HTTPS */ + +#define HTTP_STATUS_200_OK "HTTP/1.1 200 OK\r\n" \ + "Content-Type: text/html\r\n" \ + "Transfer-Encoding: chunked\r\n" + +#define HTTP_STATUS_200_OK_CSS \ + "HTTP/1.1 200 OK\r\n" \ + "Content-Type: text/css\r\n" \ + "Transfer-Encoding: chunked\r\n" + +#define HTML_HEADER "" \ + "Zephyr HTTP Server" \ + "

" \ + "
Zephyr HTTP websocket server

\r\n" + +#define HTML_FOOTER "\r\n" + +static int ws_works(struct http_ctx *ctx, const struct sockaddr *dst) +{ + NET_INFO("WS url called"); + + return 0; +} + +static void ws_connected(struct http_ctx *ctx, + enum http_connection_type type, + const struct sockaddr *dst, + void *user_data) +{ + char url[32]; + int len = min(sizeof(url), ctx->http.url_len); + + memcpy(url, ctx->http.url, len); + url[len] = '\0'; + + NET_DBG("%s connect attempt URL %s", + type == HTTP_CONNECTION ? "HTTP" : "WS", url); + + if (type == HTTP_CONNECTION) { + return; + + } else if (type == WS_CONNECTION) { + if (strncmp(ctx->http.url, "/ws", + sizeof("/ws") - 1) == 0) { + ws_works(ctx, dst); + return; + } + } +} + +static void ws_received(struct http_ctx *ctx, + struct net_pkt *pkt, + int status, + u32_t flags, + const struct sockaddr *dst, + void *user_data) +{ + if (!status) { + struct net_buf *frag; + enum ws_opcode opcode; + int ret, hdr_len; + + NET_DBG("Received %d bytes data", net_pkt_appdatalen(pkt)); + + if (flags & WS_FLAG_BINARY) { + opcode = WS_OPCODE_DATA_BINARY; + } else { + opcode = WS_OPCODE_DATA_TEXT; + } + + hdr_len = net_pkt_get_len(pkt) - net_pkt_appdatalen(pkt); + + frag = pkt->frags; + while (frag) { + net_hexdump("server recv", net_pkt_appdata(pkt), + net_pkt_appdatalen(pkt)); + + ret = ws_send_msg_to_client(ctx, frag->data + hdr_len, + frag->len - hdr_len, + opcode, + frag->frags ? true : false, + dst, user_data); + if (ret < 0) { + NET_DBG("Cannot send ws data (%d bytes) " + "back (%d)", + frag->len - hdr_len, ret); + } else { + NET_DBG("Sent %d bytes to client", + frag->len - hdr_len); + } + + frag = frag->frags; + + /* Websocket header is found in first fragment so + * reset the value here. + */ + hdr_len = 0; + } + + http_send_flush(ctx, user_data); + + if (pkt) { + net_pkt_unref(pkt); + } + + } else { + NET_ERR("Receive error (%d)", status); + + if (pkt) { + net_pkt_unref(pkt); + } + } +} + +static void ws_sent(struct http_ctx *ctx, + int status, + void *user_data_send, + void *user_data) +{ + NET_DBG("Data sent status %d", status); +} + +static void ws_closed(struct http_ctx *ctx, + int status, + void *user_data) +{ + NET_DBG("Connection %p closed", ctx); +} + +#if defined(CONFIG_NET_DEBUG_WEBSOCKET) && (NET_SYS_LOG_LEVEL > 3) +static const char *get_string(int str_len, const char *str) +{ + static char buf[64]; + int len = min(str_len, sizeof(buf) - 1); + + memcpy(buf, str, len); + buf[len] = '\0'; + + return buf; +} +#endif + +static enum http_verdict default_handler(struct http_ctx *ctx, + enum http_connection_type type, + const struct sockaddr *dst) +{ + NET_DBG("No handler for %s URL %s", + type == HTTP_CONNECTION ? "HTTP" : "WS", + get_string(ctx->http.url_len, ctx->http.url)); + + return HTTP_VERDICT_DROP; +} + +void test_websocket_init_server(void) +{ + struct sockaddr addr, *server_addr; + int ret; + + /* + * There are several options here for binding to local address. + * 1) The server address can be left empty in which case the + * library will bind to both IPv4 and IPv6 addresses and to + * default port 80 or 443 if TLS is enabled. + * 2) The server address can be partially filled, meaning that + * the address can be left to 0 and port can be set to desired + * value. If the protocol family in sockaddr is set to AF_UNSPEC, + * then both IPv4 and IPv6 socket is bound. + * 3) The address can be set to some real value. + */ +#define ADDR_OPTION 1 + +#if ADDR_OPTION == 1 + server_addr = NULL; + + ARG_UNUSED(addr); + +#elif ADDR_OPTION == 2 + /* Accept any local listening address */ + memset(&addr, 0, sizeof(addr)); + + net_sin(&addr)->sin_port = htons(ZEPHYR_PORT); + + /* In this example, listen both IPv4 and IPv6 */ + addr.sa_family = AF_UNSPEC; + + server_addr = &addr; + +#elif ADDR_OPTION == 3 + /* Set the bind address according to your configuration */ + memset(&addr, 0, sizeof(addr)); + + /* In this example, listen only IPv6 */ + addr.sa_family = AF_INET6; + net_sin6(&addr)->sin6_port = htons(ZEPHYR_PORT); + + ret = net_ipaddr_parse(ZEPHYR_ADDR, &addr); + if (ret < 0) { + NET_ERR("Cannot set local address (%d)", ret); + panic(NULL); + } + + server_addr = &addr; + +#else + server_addr = NULL; + + ARG_UNUSED(addr); +#endif + + http_server_add_default(&http_urls, default_handler); + http_server_add_url(&http_urls, "/ws", HTTP_URL_STANDARD); + http_server_add_url(&http_urls, "/ws", HTTP_URL_WEBSOCKET); + + ret = http_server_init(&http_ctx, &http_urls, server_addr, + result, sizeof(result), + "Zephyr WS server", NULL); + if (ret < 0) { + NET_ERR("Cannot init web server (%d)", ret); + return; + } + + http_set_cb(&http_ctx, ws_connected, ws_received, ws_sent, ws_closed); + +#if defined(CONFIG_NET_CONTEXT_NET_PKT_POOL) + net_app_set_net_pkt_pool(&http_ctx.app_ctx, tx_tcp_slab, data_tcp_pool); +#endif + +#if defined(CONFIG_NET_APP_TLS) + ret = http_server_set_tls(&http_ctx, + APP_BANNER, + INSTANCE_INFO, + strlen(INSTANCE_INFO), + setup_cert, + NULL, + &ssl_pool, + ws_tls_stack, + K_THREAD_STACK_SIZEOF(ws_tls_stack)); + if (ret < 0) { + NET_ERR("Cannot enable TLS support (%d)", ret); + } +#endif + + http_server_enable(&http_ctx); + + ws_ctx = &http_ctx; +} + +void test_websocket_cleanup_server(void) +{ + http_server_disable(&http_ctx); + http_release(&http_ctx); +} diff --git a/tests/net/websocket/testcase.yaml b/tests/net/websocket/testcase.yaml new file mode 100644 index 0000000000000..9666b4dff176e --- /dev/null +++ b/tests/net/websocket/testcase.yaml @@ -0,0 +1,5 @@ +tests: + - test: + build_only: true + min_ram: 32 + tags: net http websocket