diff --git a/include/net/http_client.h b/include/net/http_client.h new file mode 100644 index 0000000000000..2034465027149 --- /dev/null +++ b/include/net/http_client.h @@ -0,0 +1,250 @@ +/** @file + * @brief HTTP client API + * + * An API for applications do HTTP requests + */ + +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_NET_HTTP_CLIENT_H_ +#define ZEPHYR_INCLUDE_NET_HTTP_CLIENT_H_ + +/** + * @brief HTTP client API + * @defgroup http_client HTTP client API + * @ingroup networking + * @{ + */ + +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if !defined(HTTP_CRLF) +#define HTTP_CRLF "\r\n" +#endif + +#if !defined(HTTP_STATUS_STR_SIZE) +#define HTTP_STATUS_STR_SIZE 32 +#endif + +/* Is there more data to come */ +enum http_final_call { + HTTP_DATA_MORE = 0, + HTTP_DATA_FINAL = 1, +}; + +struct http_request; +struct http_response; + +/** + * @typedef http_payload_cb_t + * @brief Callback used when data needs to be sent to the server. + * + * @param sock Socket id of the connection + * @param req HTTP request information + * @param user_data User specified data specified in http_client_req() + * + * @return >=0 amount of data sent, in this case http_client_req() should + * continue sending data, + * <0 if http_client_req() should return the error code to the + * caller. + */ +typedef int (*http_payload_cb_t)(int sock, + struct http_request *req, + void *user_data); + +/** + * @typedef http_response_cb_t + * @brief Callback used when data is received from the server. + * + * @param sock Socket id of the connection + * @param req HTTP request information + * @param recv_buf User supplied buffer where received data is stored + * @param recv_buf_len Length of the user supplied receive buffer + * @param data_len Received data len, if this is larger than recv_buf_len, + * then some data was skipped. + * @param final_data Does this data buffer contain all the data or + * is there still more data to come. + * @param user_data User specified data specified in http_client_req() + * + * @return 0 if http_client_req() should continue processing data (if not + * all data is received), <0 if http_client_req() should return + * the error code to the caller. If all data has been received, + * then http_client_req() will return to the caller. + */ +typedef int (*http_response_cb_t)(struct http_response *rsp, + enum http_final_call final_data, + void *user_data); + +/** + * HTTP response from the server. + */ +struct http_response { + /** HTTP parser settings for the application usage */ + const struct http_parser_settings *http_cb; + + /** User provided HTTP response callback which is + * called when a response is received to a sent HTTP + * request. + */ + http_response_cb_t cb; + + /** Where the body starts */ + u8_t *body_start; + + /** Where the response is stored, this is to be + * provided by the user. + */ + u8_t *recv_buf; + + /** Response buffer maximum length */ + size_t recv_buf_len; + + /** Length of the data in the result buf. If the value + * is larger than response_buf_len, then it means that + * the data is truncated and could not be fully copied + * into response_buf. This can only happen if the user + * did not set the response callback. If the callback + * is set, then the HTTP client API will call response + * callback many times so that all the data is + * delivered to the user. + */ + size_t data_len; + + /** HTTP Content-Length field value */ + size_t content_length; + + /** Content length parsed. This should be the same as + * the content_length field if parsing was ok. + */ + size_t processed; + + /* https://tools.ietf.org/html/rfc7230#section-3.1.2 + * The status-code element is a 3-digit integer code + * + * The reason-phrase element exists for the sole + * purpose of providing a textual description + * associated with the numeric status code. A client + * SHOULD ignore the reason-phrase content. + */ + char http_status[HTTP_STATUS_STR_SIZE]; + + u8_t cl_present:1; + u8_t body_found:1; + u8_t message_complete:1; +}; + +/** + * HTTP client request. This contains all the data that is needed when doing + * a HTTP request. + */ +struct http_request { + /** HTTP parser context */ + struct http_parser parser; + + /** HTTP parser settings */ + struct http_parser_settings parser_settings; + + /** HTTP response specific data (filled by http_client_req() when + * data is received) + */ + struct http_response response; + + /** User data */ + void *user_data; + + /** HTTP socket */ + int sock; + + /** Request timeout */ + s32_t timeout; + + /* User should fill in following parameters */ + + /** The HTTP method: GET, HEAD, OPTIONS, POST, ... */ + enum http_method method; + + /** The URL for this request, for example: /index.html */ + const char *url; + + /** The HTTP protocol, for example "HTTP/1.1" */ + const char *protocol; + + /** The HTTP header fields (application specific) + * The Content-Type may be specified here or in the next field. + * Depending on your application, the Content-Type may vary, however + * some header fields may remain constant through the application's + * life cycle. This is a NULL terminated list of header fields. + */ + char **header_fields; + + /** The value of the Content-Type header field, may be NULL */ + const char *content_type_value; + + /** Hostname to be used in the request */ + const char *host; + + /** User supplied callback function to call when payload + * needs to be sent. This can be NULL in which case the payload field + * in http_request is used. The idea of this payload callback is to + * allow user to send more data that is practical to store in allocated + * memory. + */ + http_payload_cb_t payload_cb; + + /** Payload, may be NULL */ + const char *payload; + + /** Payload length, may be 0. Only used if payload field is not NULL */ + size_t payload_len; +}; + +/** + * @brief Do a HTTP request. The callback is called when data is received + * from the HTTP server. The caller must have created a connection to the + * server before calling this function so connect() call must have be done + * successfully for the socket. + * + * @param sock Socket id of the connection. + * @param req HTTP request information + * @param optional_headers A NULL terminated list of any optional headers that + * should be added to the HTTP request. + * @param response User supplied callback function to call when response is + * received + * @param http_cb User supplied list of callback functions if the + * calling application wants to know the parsing status or the HTTP + * fields. This is optional parameter and normally not needed. + * @param recv_buf User supplied buffer where received data is stored + * @param recv_buf_len Length of the user supplied receive buffer + * @param timeout Max timeout to wait for the data. The timeout value cannot be + * 0 as there would be no time to receive the data. + * @param user_data User specified data that is passed to the callback. + * + * @return <0 if error, >=0 amount of data sent to the server + */ +int http_client_req(int sock, struct http_request *req, + const char **optional_headers, + http_response_cb_t response, + const struct http_parser_settings *http_cb, + u8_t *recv_buf, size_t recv_buf_len, + s32_t timeout, void *user_data); + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* ZEPHYR_INCLUDE_NET_HTTP_CLIENT_H_ */ diff --git a/include/net/websocket.h b/include/net/websocket.h new file mode 100644 index 0000000000000..148dbab02cfab --- /dev/null +++ b/include/net/websocket.h @@ -0,0 +1,167 @@ +/** @file + * @brief Websocket API + * + * An API for applications to setup websocket connections + */ + +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_NET_WEBSOCKET_H_ +#define ZEPHYR_INCLUDE_NET_WEBSOCKET_H_ + +#include + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Websocket API + * @defgroup websocket Websocket API + * @ingroup networking + * @{ + */ + +/** Values for flag variable in HTTP receive callback */ +#define WEBSOCKET_FLAG_FINAL 0x00000001 /**< Final frame */ +#define WEBSOCKET_FLAG_TEXT 0x00000002 /**< Textual data */ +#define WEBSOCKET_FLAG_BINARY 0x00000004 /**< Binary data */ +#define WEBSOCKET_FLAG_CLOSE 0x00000008 /**< Closing connection */ +#define WEBSOCKET_FLAG_PING 0x00000010 /**< Ping message */ +#define WEBSOCKET_FLAG_PONG 0x00000011 /**< Pong message */ + +enum websocket_opcode { + WEBSOCKET_OPCODE_CONTINUE = 0x00, + WEBSOCKET_OPCODE_DATA_TEXT = 0x01, + WEBSOCKET_OPCODE_DATA_BINARY = 0x02, + WEBSOCKET_OPCODE_CLOSE = 0x08, + WEBSOCKET_OPCODE_PING = 0x09, + WEBSOCKET_OPCODE_PONG = 0x0A, +}; + +/** + * @typedef websocket_connect_cb_t + * @brief Callback called after Websocket connection is established. + * + * @param sock Websocket id + * @param req HTTP handshake request + * @param user_data A valid pointer on some user data or NULL + * + * @return 0 if ok, <0 if there is an error and connection should be aborted + */ +typedef int (*websocket_connect_cb_t)(int ws_sock, struct http_request *req, + void *user_data); + +/** + * @brief Connect to a server that provides Websocket service. The callback is + * called after connection is established. The returned value is a new socket + * value that can be used to send / receive data. + * + * @param http_sock Socket id to the server. Note that this socket is used to do + * HTTP handshakes etc. The actual Websocket connectivity is done via the + * returned websocket id. + * @param host Host of the Websocket server when doing HTTP handshakes. + * @param url URL of the Websocket. + * @param optional_headers A NULL terminated list of Any optional headers that + * should be added to the HTTP request. + * @param cb User supplied callback function to call when connection is + * established. + * @param http_cb User supplied list of callback functions if the + * calling application wants to know the parsing status or the HTTP + * fields during the handshake. This is optional parameter and normally + * not needed but is useful if the caller wants to know something about + * the fields that the server is sending. + * @param tmp_buf User supplied buffer where HTTP connection data is stored + * @param tmp_buf_len Length of the user supplied temp buffer + * @param timeout Max timeout to wait for the connection. The timeout value + * cannot be 0 as there would be no time to receive the data. + * @param user_data User specified data that is passed to the callback. + * + * @return Websocket id to be used when sending/receiving Websocket data. + */ +int websocket_connect(int http_sock, const char *host, const char *url, + const char **optional_headers, + websocket_connect_cb_t cb, + const struct http_parser_settings *http_cb, + u8_t *tmp_buf, size_t tmp_buf_len, + s32_t timeout, void *user_data); + +/** + * @brief Send websocket msg to peer. + * + * @details The function will automatically add websocket header to the + * message. + * + * @param ws_sock websocket id returned by websocket_connect(). Note that this + * socket is used to do HTTP handshakes etc. The actual Websocket + * connectivity is done via the returned websocket id. + * @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 WEBSOCKET_OPCODE_CONTINUE. If final == true and this + * is the only message, then opcode should have proper opcode (text or + * binary) set. + * @param timeout How long to try to send the message. + * + * @return <0 if error, >=0 amount of bytes sent + */ +int websocket_send_msg(int ws_sock, const u8_t *payload, size_t payload_len, + enum websocket_opcode opcode, bool mask, bool final, + s32_t timeout); + +/** + * @brief Receive websocket msg from peer. + * + * @details The function will automatically remove websocket header from the + * message. + * + * @param ws_sock websocket id returned by websocket_connect(). + * @param buf Buffer where websocket data is read. + * @param buf_len Length of the data buffer. + * @param message_type Type of the message. + * @param remaining How much there is data left in the message after this read. + * @param timeout How long to try to send the message. + * + * @return <0 if error, >=0 amount of bytes sent + */ +int websocket_recv_msg(int ws_sock, u8_t *buf, size_t buf_len, + u32_t *message_type, u64_t *remaining, s32_t timeout); + +/** + * @brief Close websocket. + * + * @details One must call websocket_connect() after this call to re-establish + * the connection. + * + * @param ws_sock websocket id returned by websocket_connect(). + */ +int websocket_disconnect(int ws_sock); + +#if defined(CONFIG_WEBSOCKET_CLIENT) +void websocket_init(void); +#else +static inline void websocket_init(void) +{ +} +#endif + +#ifdef __cplusplus +} +#endif + +/** + * @} + */ + +#endif /* ZEPHYR_INCLUDE_NET_WEBSOCKET_H_ */ diff --git a/samples/net/sockets/http_client/CMakeLists.txt b/samples/net/sockets/http_client/CMakeLists.txt new file mode 100644 index 0000000000000..a08d8e0fb6385 --- /dev/null +++ b/samples/net/sockets/http_client/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(http_client) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) + +set(gen_dir ${ZEPHYR_BINARY_DIR}/include/generated/) + +generate_inc_file_for_target( + app + src/echo-apps-cert.der + ${gen_dir}/echo-apps-cert.der.inc + ) diff --git a/samples/net/sockets/http_client/prj.conf b/samples/net/sockets/http_client/prj.conf new file mode 100644 index 0000000000000..968b5bbb8cd58 --- /dev/null +++ b/samples/net/sockets/http_client/prj.conf @@ -0,0 +1,38 @@ +# Networking config +CONFIG_NETWORKING=y +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=y +CONFIG_NET_TCP=y +CONFIG_NET_SHELL=y + +# Sockets +CONFIG_NET_SOCKETS=y +CONFIG_NET_SOCKETS_POSIX_NAMES=y +CONFIG_NET_SOCKETS_POLL_MAX=4 + +# Network driver config +CONFIG_TEST_RANDOM_GENERATOR=y + +# Network address config +CONFIG_NET_CONFIG_SETTINGS=y +CONFIG_NET_CONFIG_NEED_IPV4=y +CONFIG_NET_CONFIG_NEED_IPV6=y +CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.0.2.1" +CONFIG_NET_CONFIG_MY_IPV4_GW="192.0.2.2" +# Address of HTTP IPv4 server +CONFIG_NET_CONFIG_PEER_IPV4_ADDR="192.0.2.2" +CONFIG_NET_CONFIG_MY_IPV6_ADDR="2001:db8::1" +# Address of HTTP IPv6 server +CONFIG_NET_CONFIG_PEER_IPV6_ADDR="2001:db8::2" + +# HTTP +CONFIG_HTTP_CLIENT=y + +# Network debug config +CONFIG_LOG=y +CONFIG_LOG_IMMEDIATE=y +CONFIG_NET_LOG=y +CONFIG_NET_SOCKETS_LOG_LEVEL_DBG=n +CONFIG_NET_HTTP_LOG_LEVEL_DBG=y + +CONFIG_MAIN_STACK_SIZE=2048 diff --git a/samples/net/sockets/http_client/sample.yaml b/samples/net/sockets/http_client/sample.yaml new file mode 100644 index 0000000000000..6fa666d823861 --- /dev/null +++ b/samples/net/sockets/http_client/sample.yaml @@ -0,0 +1,8 @@ +common: + tags: net http http_client +sample: + description: HTTP client sample + name: http_client +tests: + sample.net.sockets.http_client: + harness: net diff --git a/samples/net/sockets/http_client/src/ca_certificate.h b/samples/net/sockets/http_client/src/ca_certificate.h new file mode 100644 index 0000000000000..ae99634709604 --- /dev/null +++ b/samples/net/sockets/http_client/src/ca_certificate.h @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define CA_CERTIFICATE_TAG 1 + +#define TLS_PEER_HOSTNAME "localhost" + +/* This is the same cert as what is found in net-tools/echo-apps-cert.pem file + */ +static const unsigned char ca_certificate[] = { +#include "echo-apps-cert.der.inc" +}; diff --git a/samples/net/sockets/http_client/src/echo-apps-cert.der b/samples/net/sockets/http_client/src/echo-apps-cert.der new file mode 100644 index 0000000000000..bfcb335e31c8c Binary files /dev/null and b/samples/net/sockets/http_client/src/echo-apps-cert.der differ diff --git a/samples/net/sockets/http_client/src/main.c b/samples/net/sockets/http_client/src/main.c new file mode 100644 index 0000000000000..fb4d7dd8f3e2d --- /dev/null +++ b/samples/net/sockets/http_client/src/main.c @@ -0,0 +1,335 @@ +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(net_http_client_sample, LOG_LEVEL_DBG); + +#include +#include +#include +#include + +#include "ca_certificate.h" + +#define HTTP_PORT 8000 + +#if defined(CONFIG_NET_CONFIG_PEER_IPV6_ADDR) +#define SERVER_ADDR6 CONFIG_NET_CONFIG_PEER_IPV6_ADDR +#else +#define SERVER_ADDR6 "" +#endif + +#if defined(CONFIG_NET_CONFIG_PEER_IPV4_ADDR) +#define SERVER_ADDR4 CONFIG_NET_CONFIG_PEER_IPV4_ADDR +#else +#define SERVER_ADDR4 "" +#endif + +#define MAX_RECV_BUF_LEN 512 + +static u8_t recv_buf_ipv4[MAX_RECV_BUF_LEN]; +static u8_t recv_buf_ipv6[MAX_RECV_BUF_LEN]; + +static int setup_socket(sa_family_t family, const char *server, int port, + int *sock, struct sockaddr *addr, socklen_t addr_len) +{ + const char *family_str = family == AF_INET ? "IPv4" : "IPv6"; + int ret = 0; + + memset(addr, 0, addr_len); + + if (family == AF_INET) { + net_sin(addr)->sin_family = AF_INET; + net_sin(addr)->sin_port = htons(port); + inet_pton(family, server, &net_sin(addr)->sin_addr); + } else { + net_sin6(addr)->sin6_family = AF_INET6; + net_sin6(addr)->sin6_port = htons(port); + inet_pton(family, server, &net_sin6(addr)->sin6_addr); + } + + if (IS_ENABLED(CONFIG_NET_SOCKETS_SOCKOPT_TLS)) { + sec_tag_t sec_tag_list[] = { + CA_CERTIFICATE_TAG, + }; + + *sock = socket(family, SOCK_STREAM, IPPROTO_TLS_1_2); + if (*sock >= 0) { + ret = setsockopt(*sock, SOL_TLS, TLS_SEC_TAG_LIST, + sec_tag_list, sizeof(sec_tag_list)); + if (ret < 0) { + LOG_ERR("Failed to set %s secure option (%d)", + family_str, -errno); + ret = -errno; + } + + ret = setsockopt(*sock, SOL_TLS, TLS_HOSTNAME, + TLS_PEER_HOSTNAME, + sizeof(TLS_PEER_HOSTNAME)); + if (ret < 0) { + LOG_ERR("Failed to set %s TLS_HOSTNAME " + "option (%d)", family_str, -errno); + ret = -errno; + } } + } else { + *sock = socket(family, SOCK_STREAM, IPPROTO_TCP); + } + + if (*sock < 0) { + LOG_ERR("Failed to create %s HTTP socket (%d)", family_str, + -errno); + } + + return ret; +} + +static int payload_cb(int sock, struct http_request *req, void *user_data) +{ + const char *content[] = { + "foobar", + "chuncked", + "last" + }; + char tmp[64]; + int i, pos = 0; + + for (i = 0; i < ARRAY_SIZE(content); i++) { + pos += snprintk(tmp + pos, sizeof(tmp) - pos, + "%x\r\n%s\r\n", + (unsigned int)strlen(content[i]), + content[i]); + } + + pos += snprintk(tmp + pos, sizeof(tmp) - pos, "0\r\n\r\n"); + + (void)send(sock, tmp, pos, 0); + + return pos; +} + +static int response_cb(struct http_response *rsp, + enum http_final_call final_data, + void *user_data) +{ + if (final_data == HTTP_DATA_MORE) { + LOG_INF("Partial data received (%zd bytes)", rsp->data_len); + } else if (final_data == HTTP_DATA_FINAL) { + LOG_INF("All the data received (%zd bytes)", rsp->data_len); + } + + LOG_INF("Response to %s", (const char *)user_data); + + return 0; +} + +static int connect_socket(sa_family_t family, const char *server, int port, + int *sock, struct sockaddr *addr, socklen_t addr_len) +{ + int ret; + + ret = setup_socket(family, server, port, sock, addr, addr_len); + if (ret < 0 || *sock < 0) { + return -1; + } + + ret = connect(*sock, addr, addr_len); + if (ret < 0) { + LOG_ERR("Cannot connect to %s remote (%d)", + family == AF_INET ? "IPv4" : "IPv6", + -errno); + ret = -errno; + } + + return ret; +} + +void main(void) +{ + struct sockaddr_in6 addr6; + struct sockaddr_in addr4; + int sock4 = -1, sock6 = -1; + s32_t timeout = K_FOREVER; + int ret; + + if (IS_ENABLED(CONFIG_NET_IPV4)) { + (void)connect_socket(AF_INET, SERVER_ADDR4, HTTP_PORT, + &sock4, (struct sockaddr *)&addr4, + sizeof(addr4)); + } + + if (IS_ENABLED(CONFIG_NET_IPV6)) { + (void)connect_socket(AF_INET6, SERVER_ADDR6, HTTP_PORT, + &sock6, (struct sockaddr *)&addr6, + sizeof(addr6)); + } + + if (sock4 < 0 && sock6 < 0) { + LOG_ERR("Cannot create HTTP connection."); + exit(1); + } + + if (sock4 >= 0 && IS_ENABLED(CONFIG_NET_IPV4)) { + struct http_request req; + + memset(&req, 0, sizeof(req)); + + req.method = HTTP_GET; + req.url = "/"; + req.host = SERVER_ADDR4; + req.protocol = "HTTP/1.1"; + + ret = http_client_req(sock4, &req, NULL, response_cb, NULL, + recv_buf_ipv4, sizeof(recv_buf_ipv4), + timeout, "IPv4 GET"); + + close(sock4); + } + + if (sock6 >= 0 && IS_ENABLED(CONFIG_NET_IPV6)) { + struct http_request req; + + memset(&req, 0, sizeof(req)); + + req.method = HTTP_GET; + req.url = "/"; + req.host = SERVER_ADDR6; + req.protocol = "HTTP/1.1"; + + ret = http_client_req(sock6, &req, NULL, response_cb, NULL, + recv_buf_ipv6, sizeof(recv_buf_ipv6), + timeout, "IPv6 GET"); + + close(sock6); + } + + sock4 = -1; + sock6 = -1; + + if (IS_ENABLED(CONFIG_NET_IPV4)) { + (void)connect_socket(AF_INET, SERVER_ADDR4, HTTP_PORT, + &sock4, (struct sockaddr *)&addr4, + sizeof(addr4)); + } + + if (IS_ENABLED(CONFIG_NET_IPV6)) { + (void)connect_socket(AF_INET6, SERVER_ADDR6, HTTP_PORT, + &sock6, (struct sockaddr *)&addr6, + sizeof(addr6)); + } + + if (sock4 < 0 && sock6 < 0) { + LOG_ERR("Cannot create HTTP connection."); + exit(1); + } + + if (sock4 >= 0 && IS_ENABLED(CONFIG_NET_IPV4)) { + struct http_request req; + + memset(&req, 0, sizeof(req)); + + req.method = HTTP_POST; + req.url = "/foobar"; + req.host = SERVER_ADDR4; + req.protocol = "HTTP/1.1"; + req.payload = "foobar"; + req.payload_len = strlen(req.payload); + + ret = http_client_req(sock4, &req, NULL, response_cb, NULL, + recv_buf_ipv4, sizeof(recv_buf_ipv4), + timeout, "IPv4 POST"); + + close(sock4); + } + + if (sock6 >= 0 && IS_ENABLED(CONFIG_NET_IPV6)) { + struct http_request req; + + memset(&req, 0, sizeof(req)); + + req.method = HTTP_POST; + req.url = "/"; + req.host = SERVER_ADDR6; + req.protocol = "HTTP/1.1"; + req.payload = "foobar"; + req.payload_len = strlen(req.payload); + + ret = http_client_req(sock6, &req, NULL, response_cb, NULL, + recv_buf_ipv6, sizeof(recv_buf_ipv6), + timeout, "IPv6 POST"); + + close(sock6); + } + + /* Do a chuncked POST request */ + + sock4 = -1; + sock6 = -1; + + if (IS_ENABLED(CONFIG_NET_IPV4)) { + (void)connect_socket(AF_INET, SERVER_ADDR4, HTTP_PORT, + &sock4, (struct sockaddr *)&addr4, + sizeof(addr4)); + } + + if (IS_ENABLED(CONFIG_NET_IPV6)) { + (void)connect_socket(AF_INET6, SERVER_ADDR6, HTTP_PORT, + &sock6, (struct sockaddr *)&addr6, + sizeof(addr6)); + } + + if (sock4 < 0 && sock6 < 0) { + LOG_ERR("Cannot create HTTP connection."); + exit(1); + } + + if (sock4 >= 0 && IS_ENABLED(CONFIG_NET_IPV4)) { + struct http_request req; + const char *headers[] = { + "Transfer-Encoding: chunked\r\n", + NULL + }; + + memset(&req, 0, sizeof(req)); + + req.method = HTTP_POST; + req.url = "/chuncked-test"; + req.host = SERVER_ADDR4; + req.protocol = "HTTP/1.1"; + req.payload_cb = payload_cb; + req.header_fields = headers; + + ret = http_client_req(sock4, &req, NULL, response_cb, NULL, + recv_buf_ipv4, sizeof(recv_buf_ipv4), + timeout, "IPv4 POST"); + + close(sock4); + } + + if (sock6 >= 0 && IS_ENABLED(CONFIG_NET_IPV6)) { + struct http_request req; + const char *headers[] = { + "Transfer-Encoding: chunked\r\n", + NULL + }; + + memset(&req, 0, sizeof(req)); + + req.method = HTTP_POST; + req.url = "/chuncked-test"; + req.host = SERVER_ADDR6; + req.protocol = "HTTP/1.1"; + req.payload_cb = payload_cb; + req.header_fields = headers; + + ret = http_client_req(sock6, &req, NULL, response_cb, NULL, + recv_buf_ipv6, sizeof(recv_buf_ipv6), + timeout, "IPv6 POST"); + + close(sock6); + } + + k_sleep(K_FOREVER); +} diff --git a/samples/net/sockets/websocket_client/CMakeLists.txt b/samples/net/sockets/websocket_client/CMakeLists.txt new file mode 100644 index 0000000000000..a08d8e0fb6385 --- /dev/null +++ b/samples/net/sockets/websocket_client/CMakeLists.txt @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.13.1) +include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) +project(http_client) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) + +set(gen_dir ${ZEPHYR_BINARY_DIR}/include/generated/) + +generate_inc_file_for_target( + app + src/echo-apps-cert.der + ${gen_dir}/echo-apps-cert.der.inc + ) diff --git a/samples/net/sockets/websocket_client/prj.conf b/samples/net/sockets/websocket_client/prj.conf new file mode 100644 index 0000000000000..a839cb4f4b40d --- /dev/null +++ b/samples/net/sockets/websocket_client/prj.conf @@ -0,0 +1,44 @@ +# Networking config +CONFIG_NETWORKING=y +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=y +CONFIG_NET_TCP=y +CONFIG_NET_SHELL=y +CONFIG_NET_STATISTICS=y + +# Sockets +CONFIG_NET_SOCKETS=y +CONFIG_NET_SOCKETS_POSIX_NAMES=y +CONFIG_NET_SOCKETS_POLL_MAX=4 + +# Network driver config +CONFIG_TEST_RANDOM_GENERATOR=y + +# Network address config +CONFIG_NET_CONFIG_SETTINGS=y +CONFIG_NET_CONFIG_NEED_IPV4=y +CONFIG_NET_CONFIG_NEED_IPV6=y +CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.0.2.1" +CONFIG_NET_CONFIG_MY_IPV4_GW="192.0.2.2" +# Address of HTTP IPv4 server +CONFIG_NET_CONFIG_PEER_IPV4_ADDR="192.0.2.2" +CONFIG_NET_CONFIG_MY_IPV6_ADDR="2001:db8::1" +# Address of HTTP IPv6 server +CONFIG_NET_CONFIG_PEER_IPV6_ADDR="2001:db8::2" + +# HTTP & Websocket +CONFIG_HTTP_CLIENT=y +CONFIG_WEBSOCKET_CLIENT=y + +# Network debug config +CONFIG_LOG=y +CONFIG_LOG_IMMEDIATE=y +CONFIG_NET_LOG=y +#CONFIG_NET_SOCKETS_LOG_LEVEL_DBG=y +#CONFIG_NET_HTTP_LOG_LEVEL_DBG=y +#CONFIG_NET_WEBSOCKET_LOG_LEVEL_DBG=y +#CONFIG_NET_CONTEXT_LOG_LEVEL_DBG=y +#CONFIG_NET_TCP_LOG_LEVEL_DBG=y + +CONFIG_MAIN_STACK_SIZE=2048 +CONFIG_HEAP_MEM_POOL_SIZE=1500 diff --git a/samples/net/sockets/websocket_client/sample.yaml b/samples/net/sockets/websocket_client/sample.yaml new file mode 100644 index 0000000000000..f0c1070858a89 --- /dev/null +++ b/samples/net/sockets/websocket_client/sample.yaml @@ -0,0 +1,8 @@ +common: + tags: net http http_client websocket_client +sample: + description: Websocket client sample + name: websocket_client +tests: + sample.net.sockets.websocket_client: + harness: net diff --git a/samples/net/sockets/websocket_client/src/ca_certificate.h b/samples/net/sockets/websocket_client/src/ca_certificate.h new file mode 100644 index 0000000000000..ae99634709604 --- /dev/null +++ b/samples/net/sockets/websocket_client/src/ca_certificate.h @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define CA_CERTIFICATE_TAG 1 + +#define TLS_PEER_HOSTNAME "localhost" + +/* This is the same cert as what is found in net-tools/echo-apps-cert.pem file + */ +static const unsigned char ca_certificate[] = { +#include "echo-apps-cert.der.inc" +}; diff --git a/samples/net/sockets/websocket_client/src/echo-apps-cert.der b/samples/net/sockets/websocket_client/src/echo-apps-cert.der new file mode 100644 index 0000000000000..bfcb335e31c8c Binary files /dev/null and b/samples/net/sockets/websocket_client/src/echo-apps-cert.der differ diff --git a/samples/net/sockets/websocket_client/src/main.c b/samples/net/sockets/websocket_client/src/main.c new file mode 100644 index 0000000000000..db188af1842ce --- /dev/null +++ b/samples/net/sockets/websocket_client/src/main.c @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(net_websocket_client_sample, LOG_LEVEL_DBG); + +#include +#include +#include +#include +#include + +#include "ca_certificate.h" + +#define PORT 9001 + +#if defined(CONFIG_NET_CONFIG_PEER_IPV6_ADDR) +#define SERVER_ADDR6 CONFIG_NET_CONFIG_PEER_IPV6_ADDR +#else +#define SERVER_ADDR6 "" +#endif + +#if defined(CONFIG_NET_CONFIG_PEER_IPV4_ADDR) +#define SERVER_ADDR4 CONFIG_NET_CONFIG_PEER_IPV4_ADDR +#else +#define SERVER_ADDR4 "" +#endif + +/* Generated by http://www.lipsum.com/ + * 2 paragraphs, 178 words, 1160 bytes of Lorem Ipsum + */ +const char lorem_ipsum[] = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + "Vestibulum ultricies sapien tellus, ac viverra dolor bibendum " + "lacinia. Vestibulum et nisl tristique tellus finibus gravida " + "vitae sit amet nunc. Suspendisse maximus justo mi, vitae porta " + "risus suscipit vitae. Curabitur ut fringilla velit. Donec ac nisi " + "in dui semper lobortis sed nec ante. Sed nec luctus dui. Sed ut " + "ante nisi. Mauris congue euismod felis, et maximus ex pellentesque " + "nec. Proin nibh nisl, semper at nunc in, mattis pharetra metus. Nam " + "turpis risus, pulvinar sit amet varius ac, pellentesque quis purus." + "\n" + "Nam consequat purus in lacinia fringilla. Morbi volutpat, tellus " + "nec tempus dapibus, ante sem aliquam dui, eu feugiat libero diam " + "at leo. Sed suscipit egestas orci in ultrices. Integer in elementum " + "ligula, vel sollicitudin velit. Nullam sit amet eleifend libero. " + "Proin sit amet consequat tellus, vel vulputate arcu. Curabitur quis " + "lobortis lacus. Sed faucibus vestibulum enim vel elementum. Vivamus " + "enim nunc, auctor in purus at, aliquet pulvinar eros. Cras dapibus " + "nec quam laoreet sagittis. Quisque dictum ante odio, at imperdiet " + "est convallis a. Morbi mattis ut orci vitae volutpat." + "\n"; + +#define MAX_RECV_BUF_LEN (sizeof(lorem_ipsum) - 1) + +const int ipsum_len = MAX_RECV_BUF_LEN; + +static u8_t recv_buf_ipv4[MAX_RECV_BUF_LEN]; +static u8_t recv_buf_ipv6[MAX_RECV_BUF_LEN]; + +static u8_t temp_recv_buf_ipv4[MAX_RECV_BUF_LEN]; +static u8_t temp_recv_buf_ipv6[MAX_RECV_BUF_LEN]; + +static int setup_socket(sa_family_t family, const char *server, int port, + int *sock, struct sockaddr *addr, socklen_t addr_len) +{ + const char *family_str = family == AF_INET ? "IPv4" : "IPv6"; + int ret = 0; + + memset(addr, 0, addr_len); + + if (family == AF_INET) { + net_sin(addr)->sin_family = AF_INET; + net_sin(addr)->sin_port = htons(port); + inet_pton(family, server, &net_sin(addr)->sin_addr); + } else { + net_sin6(addr)->sin6_family = AF_INET6; + net_sin6(addr)->sin6_port = htons(port); + inet_pton(family, server, &net_sin6(addr)->sin6_addr); + } + + if (IS_ENABLED(CONFIG_NET_SOCKETS_SOCKOPT_TLS)) { + sec_tag_t sec_tag_list[] = { + CA_CERTIFICATE_TAG, + }; + + *sock = socket(family, SOCK_STREAM, IPPROTO_TLS_1_2); + if (*sock >= 0) { + ret = setsockopt(*sock, SOL_TLS, TLS_SEC_TAG_LIST, + sec_tag_list, sizeof(sec_tag_list)); + if (ret < 0) { + LOG_ERR("Failed to set %s secure option (%d)", + family_str, -errno); + ret = -errno; + } + + ret = setsockopt(*sock, SOL_TLS, TLS_HOSTNAME, + TLS_PEER_HOSTNAME, + sizeof(TLS_PEER_HOSTNAME)); + if (ret < 0) { + LOG_ERR("Failed to set %s TLS_HOSTNAME " + "option (%d)", family_str, -errno); + ret = -errno; + } } + } else { + *sock = socket(family, SOCK_STREAM, IPPROTO_TCP); + } + + if (*sock < 0) { + LOG_ERR("Failed to create %s HTTP socket (%d)", family_str, + -errno); + } + + return ret; +} + +static int connect_socket(sa_family_t family, const char *server, int port, + int *sock, struct sockaddr *addr, socklen_t addr_len) +{ + int ret; + + ret = setup_socket(family, server, port, sock, addr, addr_len); + if (ret < 0 || *sock < 0) { + return -1; + } + + ret = connect(*sock, addr, addr_len); + if (ret < 0) { + LOG_ERR("Cannot connect to %s remote (%d)", + family == AF_INET ? "IPv4" : "IPv6", + -errno); + ret = -errno; + } + + return ret; +} + +static int connect_cb(int sock, struct http_request *req, void *user_data) +{ + LOG_INF("Websocket %d for %s connected.", sock, (char *)user_data); + + return 0; +} + +static size_t how_much_to_send(size_t max_len) +{ + size_t amount; + + do { + amount = sys_rand32_get() % max_len; + } while (amount == 0U); + + return amount; +} + +static ssize_t sendall_with_ws_api(int sock, const void *buf, size_t len) +{ + return websocket_send_msg(sock, buf, len, WEBSOCKET_OPCODE_DATA_TEXT, + true, true, K_FOREVER); +} + +static ssize_t sendall_with_bsd_api(int sock, const void *buf, size_t len) +{ + return send(sock, buf, len, 0); +} + +static void recv_data_with_ws_api(int sock, size_t amount, u8_t *buf, + size_t buf_len, const char *proto) +{ + u32_t message_type; + u64_t remaining; + int ret, read_pos; + + remaining = amount; + read_pos = 0; + + while (remaining) { + ret = websocket_recv_msg(sock, buf + read_pos, + buf_len - read_pos, + &message_type, + &remaining, + K_NO_WAIT); + if (ret <= 0) { + if (ret == -EAGAIN) { + k_sleep(K_MSEC(50)); + continue; + } + + LOG_DBG("%s connection closed while " + "waiting (%d/%d)", proto, ret, errno); + break; + } + + read_pos += ret; + } + + if (remaining == 0 && + memcmp(lorem_ipsum, buf, amount) != 0) { + LOG_ERR("%s data recv failure %zd bytes", proto, amount); + } +} + +static void recv_data_with_bsd_api(int sock, size_t amount, u8_t *buf, + size_t buf_len, const char *proto) +{ + int remaining = amount; + int ret, read_pos; + + remaining = amount; + read_pos = 0; + + while (remaining > 0) { + ret = recv(sock, buf + read_pos, buf_len - read_pos, 0); + if (ret <= 0) { + if (errno == EAGAIN || errno == ETIMEDOUT) { + k_sleep(K_MSEC(50)); + continue; + } + + LOG_DBG("%s connection closed while " + "waiting (%d/%d)", proto, ret, errno); + break; + } + + read_pos += ret; + remaining -= ret; + } + + if (remaining <= 0 && + memcmp(lorem_ipsum, buf, amount) != 0) { + LOG_ERR("%s data recv failure %zd bytes", proto, amount); + } +} + +static bool send_and_wait_data(int sock, size_t amount, const char *proto, + u8_t *buf, size_t buf_len) +{ + static int count; + int ret; + + if (sock < 0) { + return true; + } + + /* Send every second message using dedicated websocket API and generic + * BSD socket API. Real applications would not work like this but here + * we want to test both APIs. + */ + if (count % 2) { + ret = sendall_with_ws_api(sock, lorem_ipsum, amount); + } else { + ret = sendall_with_bsd_api(sock, lorem_ipsum, amount); + } + + if (ret <= 0) { + if (ret < 0) { + LOG_ERR("%s failed to send data using %s (%d)", proto, + (count % 2) ? "ws API" : "socket API", ret); + } else { + LOG_DBG("%s connection closed", proto); + } + + return false; + } else { + LOG_DBG("%s sent %d bytes", proto, ret); + } + + if (count % 2) { + recv_data_with_ws_api(sock, amount, buf, buf_len, proto); + } else { + recv_data_with_bsd_api(sock, amount, buf, buf_len, proto); + } + + count++; + + return true; +} + +void main(void) +{ + int sock4 = -1, sock6 = -1; + int websock4 = -1, websock6 = -1; + s32_t timeout = K_FOREVER; + struct sockaddr_in6 addr6; + struct sockaddr_in addr4; + size_t amount; + + if (IS_ENABLED(CONFIG_NET_IPV4)) { + (void)connect_socket(AF_INET, SERVER_ADDR4, PORT, + &sock4, (struct sockaddr *)&addr4, + sizeof(addr4)); + } + + if (IS_ENABLED(CONFIG_NET_IPV6)) { + (void)connect_socket(AF_INET6, SERVER_ADDR6, PORT, + &sock6, (struct sockaddr *)&addr6, + sizeof(addr6)); + } + + if (sock4 < 0 && sock6 < 0) { + LOG_ERR("Cannot create HTTP connection."); + exit(1); + } + + if (sock4 >= 0 && IS_ENABLED(CONFIG_NET_IPV4)) { + websock4 = websocket_connect(sock4, SERVER_ADDR4, "/", NULL, + connect_cb, NULL, + temp_recv_buf_ipv4, + sizeof(temp_recv_buf_ipv4), + timeout, "IPv4"); + if (websock4 < 0) { + LOG_ERR("Cannot connect to %s:%d", SERVER_ADDR4, PORT); + close(sock4); + } + } + + if (sock6 >= 0 && IS_ENABLED(CONFIG_NET_IPV6)) { + websock6 = websocket_connect(sock6, SERVER_ADDR6, "/", NULL, + connect_cb, NULL, + temp_recv_buf_ipv6, + sizeof(temp_recv_buf_ipv6), + timeout, "IPv6"); + if (websock6 < 0) { + LOG_ERR("Cannot connect to [%s]:%d", SERVER_ADDR6, + PORT); + close(sock6); + } + } + + if (websock4 < 0 && websock6 < 0) { + LOG_ERR("No IPv4 or IPv6 connectivity"); + exit(1); + } + + LOG_INF("Websocket IPv4 %d IPv6 %d", websock4, websock6); + + while (1) { + amount = how_much_to_send(ipsum_len); + + if (websock4 >= 0 && + !send_and_wait_data(websock4, amount, "IPv4", + recv_buf_ipv4, + sizeof(recv_buf_ipv4))) { + break; + } + + if (websock6 >= 0 && + !send_and_wait_data(websock6, amount, "IPv6", + recv_buf_ipv6, + sizeof(recv_buf_ipv6))) { + break; + } + + k_sleep(K_MSEC(250)); + } + + if (websock4 >= 0) { + close(websock4); + } + + if (websock6 >= 0) { + close(websock6); + } +} diff --git a/subsys/net/ip/CMakeLists.txt b/subsys/net/ip/CMakeLists.txt index 4d58a4aee935a..386598b134edd 100644 --- a/subsys/net/ip/CMakeLists.txt +++ b/subsys/net/ip/CMakeLists.txt @@ -35,5 +35,6 @@ zephyr_library_sources_ifdef(CONFIG_NET_PROMISCUOUS_MODE promiscuous.c) if(CONFIG_NET_SHELL) zephyr_library_include_directories(. ${ZEPHYR_BASE}/subsys/net/l2) +zephyr_library_include_directories(. ${ZEPHYR_BASE}/subsys/net/lib) zephyr_library_link_libraries_ifdef(CONFIG_MBEDTLS mbedTLS) endif() diff --git a/subsys/net/ip/net_core.c b/subsys/net/ip/net_core.c index fd1f4023fae0a..0a229ac3b2140 100644 --- a/subsys/net/ip/net_core.c +++ b/subsys/net/ip/net_core.c @@ -27,6 +27,7 @@ LOG_MODULE_REGISTER(net_core, CONFIG_NET_CORE_LOG_LEVEL); #include #include #include +#include #if defined(CONFIG_NET_LLDP) #include @@ -450,6 +451,7 @@ static inline int services_init(void) } dns_init_resolver(); + websocket_init(); net_shell_init(); diff --git a/subsys/net/ip/net_shell.c b/subsys/net/ip/net_shell.c index 86926ad67bc5a..c8a305af20256 100644 --- a/subsys/net/ip/net_shell.c +++ b/subsys/net/ip/net_shell.c @@ -65,6 +65,9 @@ LOG_MODULE_REGISTER(net_shell, LOG_LEVEL_DBG); #include "net_shell.h" #include "net_stats.h" +#include +#include "websocket/websocket_internal.h" + #define PR(fmt, ...) \ shell_fprintf(shell, SHELL_NORMAL, fmt, ##__VA_ARGS__) @@ -3906,6 +3909,71 @@ static int cmd_net_vlan_del(const struct shell *shell, size_t argc, return 0; } +static void websocket_context_cb(struct websocket_context *context, + void *user_data) +{ +#if defined(CONFIG_NET_IPV6) && !defined(CONFIG_NET_IPV4) +#define ADDR_LEN NET_IPV6_ADDR_LEN +#elif defined(CONFIG_NET_IPV4) && !defined(CONFIG_NET_IPV6) +#define ADDR_LEN NET_IPV4_ADDR_LEN +#else +#define ADDR_LEN NET_IPV6_ADDR_LEN +#endif + struct net_shell_user_data *data = user_data; + const struct shell *shell = data->shell; + struct net_context *net_ctx; + int *count = data->user_data; + /* +7 for []:port */ + char addr_local[ADDR_LEN + 7]; + char addr_remote[ADDR_LEN + 7] = ""; + + net_ctx = z_get_fd_obj(context->real_sock, NULL, 0); + if (net_ctx == NULL) { + PR_ERROR("Invalid fd %d"); + return; + } + + get_addresses(net_ctx, addr_local, sizeof(addr_local), + addr_remote, sizeof(addr_remote)); + + PR("[%2d] %p/%p\t%p %c%c%c %16s\t%16s\n", + (*count) + 1, context, net_ctx, + net_context_get_iface(net_ctx), + net_context_get_family(net_ctx) == AF_INET6 ? '6' : + (net_context_get_family(net_ctx) == AF_INET ? '4' : ' '), + net_context_get_type(net_ctx) == SOCK_DGRAM ? 'D' : + (net_context_get_type(net_ctx) == SOCK_STREAM ? 'S' : + (net_context_get_type(net_ctx) == SOCK_RAW ? 'R' : ' ')), + net_context_get_ip_proto(net_ctx) == IPPROTO_UDP ? 'U' : + (net_context_get_ip_proto(net_ctx) == IPPROTO_TCP ? 'T' : ' '), + addr_local, addr_remote); + + (*count)++; +} + +static int cmd_net_websocket(const struct shell *shell, size_t argc, + char *argv[]) +{ + struct net_shell_user_data user_data; + int count = 0; + + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + PR(" websocket/net_ctx\tIface Local \tRemote\n"); + + user_data.shell = shell; + user_data.user_data = &count; + + websocket_context_foreach(websocket_context_cb, &user_data); + + if (count == 0) { + PR("No connections\n"); + } + + return 0; +} + SHELL_STATIC_SUBCMD_SET_CREATE(net_cmd_arp, SHELL_CMD(flush, NULL, "Remove all entries from ARP cache.", cmd_net_arp_flush), @@ -4221,6 +4289,9 @@ SHELL_STATIC_SUBCMD_SET_CREATE(net_commands, SHELL_CMD(tcp, &net_cmd_tcp, "Connect/send/close TCP connection.", cmd_net_tcp), SHELL_CMD(vlan, &net_cmd_vlan, "Show VLAN information.", cmd_net_vlan), + SHELL_CMD(websocket, NULL, "Print information about WebSocket " + "connections.", + cmd_net_websocket), SHELL_SUBCMD_SET_END ); diff --git a/subsys/net/lib/CMakeLists.txt b/subsys/net/lib/CMakeLists.txt index b12a80000a5fe..0f29a6a36af3d 100644 --- a/subsys/net/lib/CMakeLists.txt +++ b/subsys/net/lib/CMakeLists.txt @@ -17,9 +17,9 @@ if (CONFIG_DNS_RESOLVER add_subdirectory(dns) endif() -if(CONFIG_HTTP_PARSER_URL - OR CONFIG_HTTP_PARSER) +if(CONFIG_HTTP_PARSER_URL OR CONFIG_HTTP_PARSER OR CONFIG_HTTP_CLIENT) add_subdirectory(http) endif() +add_subdirectory_ifdef(CONFIG_WEBSOCKET_CLIENT websocket) add_subdirectory_ifdef(CONFIG_OPENTHREAD_PLAT openthread) diff --git a/subsys/net/lib/Kconfig b/subsys/net/lib/Kconfig index 5e4ba3eea1b0d..3510a377092dd 100644 --- a/subsys/net/lib/Kconfig +++ b/subsys/net/lib/Kconfig @@ -14,6 +14,8 @@ source "subsys/net/lib/mqtt/Kconfig" source "subsys/net/lib/http/Kconfig" +source "subsys/net/lib/websocket/Kconfig" + source "subsys/net/lib/lwm2m/Kconfig" source "subsys/net/lib/socks/Kconfig" diff --git a/subsys/net/lib/http/CMakeLists.txt b/subsys/net/lib/http/CMakeLists.txt index e178d62f4faf2..7a2255db70459 100644 --- a/subsys/net/lib/http/CMakeLists.txt +++ b/subsys/net/lib/http/CMakeLists.txt @@ -6,5 +6,8 @@ if(CONFIG_HTTP_PARSER_STRICT) zephyr_library_compile_definitions(HTTP_PARSER_STRICT) endif() +zephyr_include_directories(${ZEPHYR_BASE}/subsys/net/ip) + zephyr_library_sources_if_kconfig(http_parser.c) zephyr_library_sources_if_kconfig(http_parser_url.c) +zephyr_library_sources_if_kconfig(http_client.c) diff --git a/subsys/net/lib/http/Kconfig b/subsys/net/lib/http/Kconfig index 10d3d5c9264b0..0f76718a34730 100644 --- a/subsys/net/lib/http/Kconfig +++ b/subsys/net/lib/http/Kconfig @@ -23,3 +23,16 @@ config HTTP_PARSER_STRICT depends on (HTTP_PARSER || HTTP_PARSER_URL) help This option enables the strict parsing option + +config HTTP_CLIENT + bool "HTTP client API [EXPERIMENTAL]" + select HTTP_PARSER + select HTTP_PARSER_URL + help + HTTP client API + +module = NET_HTTP +module-dep = NET_LOG +module-str = Log level for HTTP client library +module-help = Enables HTTP client code to output debug messages. +source "subsys/net/Kconfig.template.log_config.net" diff --git a/subsys/net/lib/http/http_client.c b/subsys/net/lib/http/http_client.c new file mode 100644 index 0000000000000..ccf25a3646440 --- /dev/null +++ b/subsys/net/lib/http/http_client.c @@ -0,0 +1,616 @@ +/** @file + * @brief HTTP client API + * + * An API for applications to send HTTP requests + */ + +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(net_http, CONFIG_NET_HTTP_LOG_LEVEL); + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "net_private.h" + +#define HTTP_CONTENT_LEN_SIZE 6 +#define MAX_SEND_BUF_LEN 192 + +static ssize_t sendall(int sock, const void *buf, size_t len) +{ + while (len) { + ssize_t out_len = send(sock, buf, len, 0); + + if (out_len < 0) { + return -errno; + } + + buf = (const char *)buf + out_len; + len -= out_len; + } + + return 0; +} + +static int http_send_data(int sock, char *send_buf, + size_t send_buf_max_len, size_t *send_buf_pos, + ...) +{ + const char *data; + va_list va; + int ret, end_of_send = *send_buf_pos; + int end_of_data, remaining_len; + + va_start(va, send_buf_pos); + + data = va_arg(va, const char *); + + while (data) { + end_of_data = 0; + + do { + int to_be_copied; + + remaining_len = strlen(data + end_of_data); + to_be_copied = send_buf_max_len - end_of_send; + + if (remaining_len > to_be_copied) { + strncpy(send_buf + end_of_send, + data + end_of_data, + to_be_copied); + + end_of_send += to_be_copied; + end_of_data += to_be_copied; + remaining_len -= to_be_copied; + + LOG_HEXDUMP_DBG(send_buf, end_of_send, + "Data to send"); + + ret = sendall(sock, send_buf, end_of_send); + if (ret < 0) { + NET_DBG("Cannot send %d bytes (%d)", + end_of_send, ret); + goto err; + } + + end_of_send = 0; + continue; + } else { + strncpy(send_buf + end_of_send, + data + end_of_data, + remaining_len); + end_of_send += remaining_len; + remaining_len = 0; + } + } while (remaining_len > 0); + + data = va_arg(va, const char *); + }; + + va_end(va); + + if (end_of_send > (int)send_buf_max_len) { + NET_ERR("Sending overflow (%d > %zd)", end_of_send, + send_buf_max_len); + return -EMSGSIZE; + } + + *send_buf_pos = end_of_send; + + return end_of_send; + +err: + va_end(va); + + return ret; +} + +static int http_flush_data(int sock, const char *send_buf, size_t send_buf_len) +{ + LOG_HEXDUMP_DBG(send_buf, send_buf_len, "Data to send"); + + return sendall(sock, send_buf, send_buf_len); +} + +static void print_header_field(size_t len, const char *str) +{ + if (IS_ENABLED(CONFIG_NET_HTTP_LOG_LEVEL_DBG)) { +#define MAX_OUTPUT_LEN 128 + char output[MAX_OUTPUT_LEN]; + + /* The value of len does not count \0 so we need to increase it + * by one. + */ + if ((len + 1) > sizeof(output)) { + len = sizeof(output) - 1; + } + + snprintk(output, len + 1, "%s", str); + + NET_DBG("[%zd] %s", len, log_strdup(output)); + } +} + +static int on_url(struct http_parser *parser, const char *at, size_t length) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + print_header_field(length, at); + + if (req->response.http_cb && req->response.http_cb->on_url) { + req->response.http_cb->on_url(parser, at, length); + } + + return 0; +} + +static int on_status(struct http_parser *parser, const char *at, size_t length) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + u16_t len; + + len = MIN(length, sizeof(req->response.http_status) - 1); + memcpy(req->response.http_status, at, len); + req->response.http_status[len] = 0; + + NET_DBG("HTTP response status %d %s", parser->status_code, + log_strdup(req->response.http_status)); + + if (req->response.http_cb && req->response.http_cb->on_status) { + req->response.http_cb->on_status(parser, at, length); + } + + return 0; +} + +static int on_header_field(struct http_parser *parser, const char *at, + size_t length) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + const char *content_len = "Content-Length"; + u16_t len; + + len = strlen(content_len); + if (length >= len && strncasecmp(at, content_len, len) == 0) { + req->response.cl_present = true; + } + + print_header_field(length, at); + + if (req->response.http_cb && req->response.http_cb->on_header_field) { + req->response.http_cb->on_header_field(parser, at, length); + } + + return 0; +} + +#define MAX_NUM_DIGITS 16 + +static int on_header_value(struct http_parser *parser, const char *at, + size_t length) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + char str[MAX_NUM_DIGITS]; + + if (req->response.cl_present) { + if (length <= MAX_NUM_DIGITS - 1) { + long int num; + + memcpy(str, at, length); + str[length] = 0; + + num = strtol(str, NULL, 10); + if (num == LONG_MIN || num == LONG_MAX) { + return -EINVAL; + } + + req->response.content_length = num; + } + + req->response.cl_present = false; + } + + if (req->response.http_cb && req->response.http_cb->on_header_value) { + req->response.http_cb->on_header_value(parser, at, length); + } + + print_header_field(length, at); + + return 0; +} + +static int on_body(struct http_parser *parser, const char *at, size_t length) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + + req->response.body_found = 1; + req->response.processed += length; + + NET_DBG("Processed %zd length %zd", req->response.processed, length); + + if (req->response.http_cb && req->response.http_cb->on_body) { + req->response.http_cb->on_body(parser, at, length); + } + + if (!req->response.body_start && + (u8_t *)at != (u8_t *)req->response.recv_buf) { + req->response.body_start = (u8_t *)at; + } + + if (req->response.cb) { + if (http_should_keep_alive(parser)) { + NET_DBG("Calling callback for partitioned %zd len data", + req->response.data_len); + + req->response.cb(&req->response, HTTP_DATA_MORE, + req->user_data); + } else { + NET_DBG("Calling callback for %zd len data", + req->response.data_len); + + req->response.cb(&req->response, HTTP_DATA_FINAL, + req->user_data); + } + + /* Re-use the result buffer and start to fill it again */ + req->response.data_len = 0; + req->response.body_start = NULL; + } + + return 0; +} + +static int on_headers_complete(struct http_parser *parser) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + + if (req->response.http_cb && + req->response.http_cb->on_headers_complete) { + req->response.http_cb->on_headers_complete(parser); + } + + if (parser->status_code >= 500 && parser->status_code < 600) { + NET_DBG("Status %d, skipping body", parser->status_code); + + return 1; + } + + if ((req->method == HTTP_HEAD || req->method == HTTP_OPTIONS) && + req->response.content_length > 0) { + NET_DBG("No body expected"); + return 1; + } + + if ((req->method == HTTP_PUT || req->method == HTTP_POST) && + req->response.content_length == 0) { + NET_DBG("No body expected"); + return 1; + } + + NET_DBG("Headers complete"); + + return 0; +} + +static int on_message_begin(struct http_parser *parser) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + + if (req->response.http_cb && req->response.http_cb->on_message_begin) { + req->response.http_cb->on_message_begin(parser); + } + + if (IS_ENABLED(CONFIG_NET_HTTP_LOG_LEVEL_DBG)) { + NET_DBG("-- HTTP %s response (headers) --", + http_method_str(req->method)); + } + + return 0; +} + +static int on_message_complete(struct http_parser *parser) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + + if (req->response.http_cb && + req->response.http_cb->on_message_complete) { + req->response.http_cb->on_message_complete(parser); + } + + NET_DBG("-- HTTP %s response (complete) --", + http_method_str(req->method)); + + req->response.message_complete = 1; + + if (req->response.cb) { + req->response.cb(&req->response, HTTP_DATA_FINAL, + req->user_data); + } + + return 0; +} + +static int on_chunk_header(struct http_parser *parser) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + + if (req->response.http_cb && req->response.http_cb->on_chunk_header) { + req->response.http_cb->on_chunk_header(parser); + } + + return 0; +} + +static int on_chunk_complete(struct http_parser *parser) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + + if (req->response.http_cb && + req->response.http_cb->on_chunk_complete) { + req->response.http_cb->on_chunk_complete(parser); + } + + return 0; +} + +static void http_client_init_parser(struct http_parser *parser, + struct http_parser_settings *settings) +{ + http_parser_init(parser, HTTP_RESPONSE); + + settings->on_body = on_body; + settings->on_chunk_complete = on_chunk_complete; + settings->on_chunk_header = on_chunk_header; + settings->on_headers_complete = on_headers_complete; + settings->on_header_field = on_header_field; + settings->on_header_value = on_header_value; + settings->on_message_begin = on_message_begin; + settings->on_message_complete = on_message_complete; + settings->on_status = on_status; + settings->on_url = on_url; +} + +static int http_wait_data(int sock, struct http_request *req) +{ + int total_received = 0; + size_t offset = 0; + int received, ret; + + do { + received = recv(sock, req->response.recv_buf + offset, + req->response.recv_buf_len - offset, 0); + if (received == 0) { + /* Connection closed */ + LOG_DBG("Connection closed"); + ret = total_received; + break; + } else if (received < 0) { + /* Socket error */ + LOG_DBG("Connection error (%d)", errno); + ret = -errno; + break; + } else { + req->response.data_len += received; + + (void)http_parser_execute(&req->parser, + &req->parser_settings, + req->response.recv_buf + offset, + received); + } + + total_received += received; + offset += received; + + if (offset > req->response.recv_buf_len) { + offset = 0; + } + + if (req->response.message_complete) { + ret = total_received; + break; + } + + } while (true); + + return ret; +} + +int http_client_req(int sock, struct http_request *req, + const char **optional_headers, + http_response_cb_t response, + const struct http_parser_settings *http_cb, + u8_t *recv_buf, size_t recv_buf_len, + s32_t timeout, void *user_data) +{ + /* Utilize the network usage by sending data in bigger blocks */ + char send_buf[MAX_SEND_BUF_LEN]; + const size_t send_buf_max_len = sizeof(send_buf); + size_t send_buf_pos = 0; + int total_sent = 0; + int ret, total_recv, i; + const char *method; + + if (sock < 0 || req == NULL || response == NULL || + recv_buf == NULL || recv_buf_len == 0) { + return -EINVAL; + } + + memset(&req->response, 0, sizeof(req->response)); + + req->response.http_cb = http_cb; + req->response.cb = response; + req->response.recv_buf = recv_buf; + req->response.recv_buf_len = recv_buf_len; + req->user_data = user_data; + req->timeout = timeout; + req->sock = sock; + + method = http_method_str(req->method); + + ret = http_send_data(sock, send_buf, send_buf_max_len, &send_buf_pos, + method, " ", req->url, " ", req->protocol, + HTTP_CRLF, NULL); + if (ret < 0) { + goto out; + } + + total_sent += ret; + + ret = http_send_data(sock, send_buf, send_buf_max_len, &send_buf_pos, + "Host", ": ", req->host, HTTP_CRLF, NULL); + if (ret < 0) { + goto out; + } + + total_sent += ret; + + for (i = 0; optional_headers && optional_headers[i]; i++) { + ret = http_send_data(sock, send_buf, send_buf_max_len, + &send_buf_pos, optional_headers[i], NULL); + if (ret < 0) { + goto out; + } + + total_sent += ret; + } + + for (i = 0; req->header_fields && req->header_fields[i]; i++) { + ret = http_send_data(sock, send_buf, send_buf_max_len, + &send_buf_pos, req->header_fields[i], + NULL); + if (ret < 0) { + goto out; + } + + total_sent += ret; + } + + if (req->content_type_value) { + ret = http_send_data(sock, send_buf, send_buf_max_len, + &send_buf_pos, "Content-Type", ": ", + req->content_type_value, HTTP_CRLF, NULL); + if (ret < 0) { + goto out; + } + + total_sent += ret; + } + + if (req->payload_cb) { + ret = http_send_data(sock, send_buf, send_buf_max_len, + &send_buf_pos, HTTP_CRLF, NULL); + if (ret < 0) { + goto out; + } + + total_sent += ret; + + ret = http_flush_data(sock, send_buf, send_buf_pos); + if (ret < 0) { + goto out; + } + + send_buf_pos = 0; + total_sent += ret; + + ret = req->payload_cb(sock, req, user_data); + if (ret < 0) { + goto out; + } + + total_sent += ret; + } else if (req->payload) { + char content_len_str[HTTP_CONTENT_LEN_SIZE]; + + ret = snprintk(content_len_str, HTTP_CONTENT_LEN_SIZE, + "%zd", req->payload_len); + if (ret <= 0 || ret >= HTTP_CONTENT_LEN_SIZE) { + ret = -ENOMEM; + goto out; + } + + ret = http_send_data(sock, send_buf, send_buf_max_len, + &send_buf_pos, "Content-Length", ": ", + content_len_str, HTTP_CRLF, + HTTP_CRLF, NULL); + if (ret < 0) { + goto out; + } + + total_sent += ret; + + ret = http_send_data(sock, send_buf, send_buf_max_len, + &send_buf_pos, req->payload, NULL); + if (ret < 0) { + goto out; + } + + total_sent += ret; + } else { + ret = http_send_data(sock, send_buf, send_buf_max_len, + &send_buf_pos, HTTP_CRLF, HTTP_CRLF, + NULL); + if (ret < 0) { + goto out; + } + } + + if (send_buf_pos > 0) { + ret = http_flush_data(sock, send_buf, send_buf_pos); + if (ret < 0) { + goto out; + } + + total_sent += ret; + } + + NET_DBG("Sent %d bytes", total_sent); + + http_client_init_parser(&req->parser, &req->parser_settings); + + /* Request is sent, now wait data to be received */ + total_recv = http_wait_data(sock, req); + if (total_recv < 0) { + NET_DBG("Wait data failure (%d)", total_recv); + } else { + NET_DBG("Received %d bytes", total_recv); + } + + return total_sent; + +out: + return ret; +} + diff --git a/subsys/net/lib/websocket/CMakeLists.txt b/subsys/net/lib/websocket/CMakeLists.txt new file mode 100644 index 0000000000000..aed4e4708c7a7 --- /dev/null +++ b/subsys/net/lib/websocket/CMakeLists.txt @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_library_named(websocket_client) +zephyr_include_directories(${ZEPHYR_BASE}/subsys/net/lib/sockets) + +zephyr_library_sources( + websocket.c +) + +zephyr_library_link_libraries_ifdef(CONFIG_MBEDTLS mbedTLS) diff --git a/subsys/net/lib/websocket/Kconfig b/subsys/net/lib/websocket/Kconfig new file mode 100644 index 0000000000000..5e0d59f07a6a2 --- /dev/null +++ b/subsys/net/lib/websocket/Kconfig @@ -0,0 +1,30 @@ +# Copyright (c) 2019 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 +# + +menuconfig WEBSOCKET_CLIENT + bool "Websocket client support [EXPERIMENTAL]" + select NET_SOCKETS + select HTTP_PARSER + select HTTP_CLIENT + select MBEDTLS + select BASE64 + help + Enable Websocket client library. + +if WEBSOCKET_CLIENT + +config WEBSOCKET_MAX_CONTEXTS + int "Max number of websockets to allocate" + default 1 + help + How many Websockets can be created in the system. + +module = NET_WEBSOCKET +module-dep = NET_LOG +module-str = Log level for Websocket +module-help = Enable debug message of Websocket client library. +source "subsys/net/Kconfig.template.log_config.net" + +endif diff --git a/subsys/net/lib/websocket/websocket.c b/subsys/net/lib/websocket/websocket.c new file mode 100644 index 0000000000000..570c3580018f5 --- /dev/null +++ b/subsys/net/lib/websocket/websocket.c @@ -0,0 +1,855 @@ +/** @file + * @brief Websocket client API + * + * An API for applications to setup a websocket connections. + */ + +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(net_websocket, CONFIG_NET_WEBSOCKET_LOG_LEVEL); + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "net_private.h" +#include "sockets_internal.h" +#include "websocket_internal.h" + +/* If you want to see the data that is being sent, then you can enable + * debugging and set the following variable to 1. It prints a lot of data + * so is not enabled by default. + */ +#define HEXDUMP_SENT_PACKETS 0 +#define HEXDUMP_RECV_PACKETS 0 + +static struct websocket_context contexts[CONFIG_WEBSOCKET_MAX_CONTEXTS]; + +static struct k_sem contexts_lock; + +extern const struct socket_op_vtable sock_fd_op_vtable; +static const struct socket_op_vtable websocket_fd_op_vtable; + +static int websocket_context_ref(struct websocket_context *ctx) +{ + int old_rc = atomic_inc(&ctx->refcount); + + return old_rc + 1; +} + +static int websocket_context_unref(struct websocket_context *ctx) +{ + int old_rc = atomic_dec(&ctx->refcount); + + if (old_rc != 1) { + return old_rc - 1; + } + + return 0; +} + +static inline bool websocket_context_is_used(struct websocket_context *ctx) +{ + NET_ASSERT(ctx); + + return !!atomic_get(&ctx->refcount); +} + +static struct websocket_context *websocket_get(void) +{ + struct websocket_context *ctx = NULL; + int i; + + k_sem_take(&contexts_lock, K_FOREVER); + + for (i = 0; i < ARRAY_SIZE(contexts); i++) { + if (websocket_context_is_used(&contexts[i])) { + continue; + } + + websocket_context_ref(&contexts[i]); + ctx = &contexts[i]; + break; + } + + k_sem_give(&contexts_lock); + + return ctx; +} + +static struct websocket_context *websocket_find(int real_sock) +{ + struct websocket_context *ctx = NULL; + int i; + + k_sem_take(&contexts_lock, K_FOREVER); + + for (i = 0; i < ARRAY_SIZE(contexts); i++) { + if (!websocket_context_is_used(&contexts[i])) { + continue; + } + + if (contexts[i].real_sock != real_sock) { + continue; + } + + ctx = &contexts[i]; + break; + } + + k_sem_give(&contexts_lock); + + return ctx; +} + +static int response_cb(struct http_response *rsp, + enum http_final_call final_data, + void *user_data) +{ + struct websocket_context *ctx = user_data; + + if (final_data == HTTP_DATA_MORE) { + NET_DBG("[%p] Partial data received (%zd bytes)", ctx, + rsp->data_len); + ctx->all_received = false; + } else if (final_data == HTTP_DATA_FINAL) { + NET_DBG("[%p] All the data received (%zd bytes)", ctx, + rsp->data_len); + ctx->all_received = true; + } + + return 0; +} + +static int on_header_field(struct http_parser *parser, const char *at, + size_t length) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + struct websocket_context *ctx = req->user_data; + const char *ws_accept_str = "Sec-WebSocket-Accept"; + u16_t len; + + len = strlen(ws_accept_str); + if (length >= len && strncasecmp(at, ws_accept_str, len) == 0) { + ctx->sec_accept_present = true; + } + + if (ctx->http_cb && ctx->http_cb->on_header_field) { + ctx->http_cb->on_header_field(parser, at, length); + } + + return 0; +} + +#define MAX_SEC_ACCEPT_LEN 32 + +static int on_header_value(struct http_parser *parser, const char *at, + size_t length) +{ + struct http_request *req = CONTAINER_OF(parser, + struct http_request, + parser); + struct websocket_context *ctx = req->user_data; + char str[MAX_SEC_ACCEPT_LEN]; + + if (ctx->sec_accept_present) { + int ret; + size_t olen; + + ctx->sec_accept_ok = false; + + ret = base64_encode(str, sizeof(str) - 1, &olen, + ctx->sec_accept_key, + WS_SHA1_OUTPUT_LEN); + if (ret == 0) { + if (strncmp(at, str, length)) { + NET_DBG("[%p] Security keys do not match " + "%s vs %s", ctx, str, at); + } else { + ctx->sec_accept_ok = true; + } + } + } + + if (ctx->http_cb && ctx->http_cb->on_header_value) { + ctx->http_cb->on_header_value(parser, at, length); + } + + return 0; +} + +int websocket_connect(int sock, const char *host, const char *url, + const char **optional_headers, + websocket_connect_cb_t cb, + const struct http_parser_settings *http_cb, + u8_t *tmp_buf, size_t tmp_buf_len, + s32_t timeout, void *user_data) +{ + /* This is the expected Sec-WebSocket-Accept key. We are storing a + * pointer to this in ctx but the value is only used for the duration + * of this function call so there is no issue even if this variable + * is allocated from stack. + */ + u8_t sec_accept_key[WS_SHA1_OUTPUT_LEN]; + struct http_parser_settings http_parser_settings; + struct websocket_context *ctx; + struct http_request req; + int ret, fd, key_len; + size_t olen; + char key_accept[MAX_SEC_ACCEPT_LEN + sizeof(WS_MAGIC)]; + u32_t rnd_value = sys_rand32_get(); + char sec_ws_key[] = + "Sec-WebSocket-Key: 0123456789012345678901==\r\n"; + char *headers[] = { + sec_ws_key, + "Upgrade: websocket\r\n", + "Connection: Upgrade\r\n", + "Sec-WebSocket-Version: 13\r\n", + NULL + }; + + if (sock < 0 || host == NULL || url == NULL) { + return -EINVAL; + } + + ctx = websocket_find(sock); + if (ctx) { + NET_DBG("[%p] Websocket for sock %d already exists!", ctx, + sock); + return -EEXIST; + } + + ctx = websocket_get(); + if (!ctx) { + return -ENOENT; + } + + ctx->real_sock = sock; + ctx->tmp_buf = tmp_buf; + ctx->tmp_buf_len = tmp_buf_len; + ctx->timeout = timeout; + ctx->sec_accept_key = sec_accept_key; + ctx->http_cb = http_cb; + + mbedtls_sha1_ret((const unsigned char *)&rnd_value, sizeof(rnd_value), + sec_accept_key); + + ret = base64_encode(sec_ws_key + sizeof("Sec-Websocket-Key: ") - 1, + sizeof(sec_ws_key) - + sizeof("Sec-Websocket-Key: "), + &olen, sec_accept_key, + /* We are only interested in 16 first bytes so + * substract 4 from the SHA-1 length + */ + sizeof(sec_accept_key) - 4); + if (ret) { + NET_DBG("[%p] Cannot encode base64 (%d)", ctx, ret); + goto out; + } + + if ((olen + sizeof("Sec-Websocket-Key: ") + 2) > sizeof(sec_ws_key)) { + NET_DBG("[%p] Too long message (%zd > %zd)", ctx, + olen + sizeof("Sec-Websocket-Key: ") + 2, + sizeof(sec_ws_key)); + ret = -EMSGSIZE; + goto out; + } + + memcpy(sec_ws_key + sizeof("Sec-Websocket-Key: ") - 1 + olen, + HTTP_CRLF, sizeof(HTTP_CRLF)); + + memset(&req, 0, sizeof(req)); + + req.method = HTTP_GET; + req.url = url; + req.host = host; + req.protocol = "HTTP/1.1"; + req.header_fields = headers; + + /* We need to catch the Sec-WebSocket-Accept field in order to verify + * that it contains the stuff that we sent in Sec-WebSocket-Key field + * so setup HTTP callbacks so that we will get the needed fields. + */ + if (ctx->http_cb) { + memcpy(&http_parser_settings, ctx->http_cb, + sizeof(http_parser_settings)); + } else { + memset(&http_parser_settings, 0, sizeof(http_parser_settings)); + } + + http_parser_settings.on_header_field = on_header_field; + http_parser_settings.on_header_value = on_header_value; + + /* Pre-calculate the expected Sec-Websocket-Accept field */ + key_len = MIN(sizeof(key_accept) - 1, olen); + strncpy(key_accept, sec_ws_key + sizeof("Sec-Websocket-Key: ") - 1, + key_len); + + olen = MIN(sizeof(key_accept) - 1 - key_len, sizeof(WS_MAGIC) - 1); + strncpy(key_accept + key_len, WS_MAGIC, olen); + + /* This SHA-1 value is then checked when we receive the response */ + mbedtls_sha1_ret(key_accept, olen + key_len, sec_accept_key); + + ret = http_client_req(sock, &req, optional_headers, response_cb, + &http_parser_settings, tmp_buf, tmp_buf_len, + timeout, ctx); + if (ret < 0) { + NET_DBG("[%p] Cannot connect to Websocket host %s", ctx, host); + ret = -ECONNABORTED; + goto out; + } + + if (!(ctx->all_received && ctx->sec_accept_ok)) { + NET_DBG("[%p] WS handshake failed (%d/%d)", ctx, + ctx->all_received, ctx->sec_accept_ok); + ret = -ECONNABORTED; + goto out; + } + + ctx->user_data = user_data; + + fd = z_reserve_fd(); + if (fd < 0) { + ret = -ENOSPC; + goto out; + } + + ctx->sock = fd; + +#ifdef CONFIG_USERSPACE + /* Set net context object as initialized and grant access to the + * calling thread (and only the calling thread) + */ + z_object_recycle(ctx); +#endif + + z_finalize_fd(fd, ctx, + (const struct fd_op_vtable *)&websocket_fd_op_vtable); + + /* Call the user specified callback and if it accepts the connection + * then continue. + */ + if (cb) { + ret = cb(fd, &req, user_data); + if (ret < 0) { + NET_DBG("[%p] Connection aborted (%d)", ctx, ret); + goto out; + } + } + + NET_DBG("[%p] WS connection to peer established (fd %d)", ctx, fd); + + return fd; + +out: + websocket_context_unref(ctx); + return ret; +} + +int websocket_disconnect(int ws_sock) +{ + struct websocket_context *ctx; + + ctx = z_get_fd_obj(ws_sock, NULL, 0); + if (ctx == NULL) { + return -ENOENT; + } + + NET_DBG("[%p] Disconnecting", ctx); + + return close(ctx->real_sock); +} + +static int websocket_ioctl_vmeth(void *obj, unsigned int request, va_list args) +{ + if (request == ZFD_IOCTL_CLOSE) { + struct websocket_context *ctx = obj; + int ret; + + ret = websocket_disconnect(ctx->sock); + if (ret < 0) { + NET_DBG("[%p] Cannot close (%d)", obj, ret); + + errno = -ret; + return -1; + } + + return ret; + } + + return sock_fd_op_vtable.fd_vtable.ioctl(obj, request, args); +} + +static void websocket_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 int websocket_prepare_and_send(struct websocket_context *ctx, + u8_t *header, size_t header_len, + u8_t *payload, size_t payload_len, + s32_t timeout) +{ + struct iovec io_vector[2]; + struct msghdr msg; + + io_vector[0].iov_base = header; + io_vector[0].iov_len = header_len; + io_vector[1].iov_base = payload; + io_vector[1].iov_len = payload_len; + + memset(&msg, 0, sizeof(msg)); + + msg.msg_iov = io_vector; + msg.msg_iovlen = ARRAY_SIZE(io_vector); + +#if HEXDUMP_SENT_PACKETS + LOG_HEXDUMP_DBG(header, header_len, "Header"); + LOG_HEXDUMP_DBG(payload, payload_len, "Payload"); +#endif + + return sendmsg(ctx->real_sock, &msg, + timeout == K_NO_WAIT ? MSG_DONTWAIT : 0); +} + +int websocket_send_msg(int ws_sock, const u8_t *payload, size_t payload_len, + enum websocket_opcode opcode, bool mask, bool final, + s32_t timeout) +{ + struct websocket_context *ctx; + u8_t header[MAX_HEADER_LEN], hdr_len = 2; + u8_t *data_to_send = (u8_t *)payload; + int ret; + + if (opcode != WEBSOCKET_OPCODE_DATA_TEXT && + opcode != WEBSOCKET_OPCODE_DATA_BINARY && + opcode != WEBSOCKET_OPCODE_CONTINUE && + opcode != WEBSOCKET_OPCODE_CLOSE && + opcode != WEBSOCKET_OPCODE_PING && + opcode != WEBSOCKET_OPCODE_PONG) { + return -EINVAL; + } + + ctx = z_get_fd_obj(ws_sock, NULL, 0); + if (ctx == NULL) { + return -EBADF; + } + + if (!PART_OF_ARRAY(contexts, ctx)) { + return -ENOENT; + } + + 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) { + ctx->masking_value = sys_rand32_get(); + + header[hdr_len++] |= ctx->masking_value >> 24; + header[hdr_len++] |= ctx->masking_value >> 16; + header[hdr_len++] |= ctx->masking_value >> 8; + header[hdr_len++] |= ctx->masking_value; + + data_to_send = k_malloc(payload_len); + if (!data_to_send) { + return -ENOMEM; + } + + memcpy(data_to_send, payload, payload_len); + + websocket_mask_payload(data_to_send, payload_len, + ctx->masking_value); + } + + ret = websocket_prepare_and_send(ctx, header, hdr_len, + data_to_send, payload_len, timeout); + if (ret < 0) { + NET_DBG("Cannot send ws msg (%d)", -errno); + goto quit; + } + +quit: + if (data_to_send != payload) { + k_free(data_to_send); + } + + return ret - hdr_len; +} + +static bool websocket_parse_header(u8_t *buf, size_t buf_len, bool *masked, + u32_t *mask_value, u64_t *message_length, + u32_t *message_type_flag, + size_t *header_len) +{ + u8_t len_len; /* length of the length field in header */ + u8_t len; /* message length byte */ + u16_t value; + + value = sys_get_be16(&buf[0]); + if (value & 0x8000) { + *message_type_flag |= WEBSOCKET_FLAG_FINAL; + } + + switch (value & 0x0f00) { + case 0x0100: + *message_type_flag |= WEBSOCKET_FLAG_TEXT; + break; + case 0x0200: + *message_type_flag |= WEBSOCKET_FLAG_BINARY; + break; + case 0x0800: + *message_type_flag |= WEBSOCKET_FLAG_CLOSE; + break; + case 0x0900: + *message_type_flag |= WEBSOCKET_FLAG_PING; + break; + case 0x0A00: + *message_type_flag |= WEBSOCKET_FLAG_PONG; + break; + } + + len = value & 0x007f; + if (len < 126) { + len_len = 0; + *message_length = len; + } else if (len == 126) { + len_len = 2; + *message_length = sys_get_be16(&buf[2]); + } else { + len_len = 8; + *message_length = sys_get_be64(&buf[2]); + } + + /* Minimum websocket header is 2 bytes, header length might be + * bigger depending on length field len. + */ + *header_len = MIN_HEADER_LEN + len_len; + + if (buf_len >= *header_len) { + if (value & 0x0080) { + *masked = true; + *mask_value = sys_get_be32(&buf[2 + len_len]); + *header_len += 4; + } else { + *masked = false; + } + + return true; + } + + return false; +} + +int websocket_recv_msg(int ws_sock, u8_t *buf, size_t buf_len, + u32_t *message_type, u64_t *remaining, s32_t timeout) +{ + struct websocket_context *ctx; + size_t header_len, pos_to_write = 0; + int ret = 0; + + ctx = z_get_fd_obj(ws_sock, NULL, 0); + if (ctx == NULL) { + return -EBADF; + } + + if (!PART_OF_ARRAY(contexts, ctx)) { + return -ENOENT; + } + + /* If we have not received the websocket header yet, read it first */ + if (!ctx->header_received) { + ret = recv(ctx->real_sock, &ctx->header[ctx->pos], + sizeof(ctx->header) - ctx->pos, + timeout == K_NO_WAIT ? MSG_DONTWAIT : 0); + if (ret < 0) { + return -errno; + } + + if (ret == 0) { + /* Socket closed */ + return 0; + } + + ctx->pos += ret; + + if (ctx->pos >= MIN_HEADER_LEN) { + bool masked; + + /* Now we will be able to figure out what is the + * actual size of the header. + */ + if (websocket_parse_header(ctx->header, + ctx->pos, + &masked, + &ctx->masking_value, + &ctx->message_len, + &ctx->message_type, + &header_len)) { + ctx->masked = masked; + } else { + return -EAGAIN; + } + } else { + return -EAGAIN; + } + + if (ctx->pos < sizeof(ctx->header) && + ctx->pos < ctx->message_len) { + ctx->pos += ret; + return -EAGAIN; + } + + /* All the header is now received, we can read the payload + * data next. + */ + ctx->header_received = true; + + /* If there is any data in the header, then move it to the + * data buffer. + */ + if (header_len < sizeof(ctx->header)) { + NET_ASSERT(ctx->pos <= sizeof(ctx->header)); + + memmove(buf, &ctx->header[header_len], + ctx->pos - header_len); + pos_to_write = ctx->pos - header_len; + } + +#if HEXDUMP_RECV_PACKETS + LOG_HEXDUMP_DBG(ctx->header, header_len, "Header"); + NET_DBG("[%p] masked %d mask 0x%04x hdr %zd msg %zd", + ctx, ctx->masked, ctx->masking_value, header_len, + (size_t)ctx->message_len); +#endif + + ctx->total_read = pos_to_write; + ctx->pos = 0; + } + + if (ctx->total_read < ctx->message_len) { + ret = recv(ctx->real_sock, &buf[pos_to_write], + buf_len - pos_to_write, + timeout == K_NO_WAIT ? MSG_DONTWAIT : 0); + if (ret < 0) { + return -errno; + } + + if (ret == 0) { + return 0; + } + + ctx->total_read += ret; + ret += pos_to_write; + } + + /* Unmask the data */ + if (ctx->masked) { + websocket_mask_payload(buf, ret, ctx->masking_value); + } + +#if HEXDUMP_RECV_PACKETS + LOG_HEXDUMP_DBG(buf, ret, "Payload"); +#endif + + /* If we receive the end of the message and there is still data left, + * store the remaining to header and tmp buffer. + */ + if (ctx->total_read >= ctx->message_len) { + ret = ctx->total_read - ctx->message_len; + if (ret > 0) { + /* Rest of the stuff needds to go to header temp + * buffer and to user specified temp buf + */ + /* FIXME */ + *remaining = 0; + } else if (ret == 0) { + ctx->header_received = false; + ret = ctx->message_len; + *remaining = 0; + } else { + return -EMSGSIZE; + } + } else { + *remaining = ctx->message_len - ctx->total_read; + } + + return ret; +} + +static int websocket_send(struct websocket_context *ctx, const u8_t *buf, + size_t buf_len, s32_t timeout) +{ + int ret; + + NET_DBG("[%p] Sending %zd bytes", ctx, buf_len); + + ret = websocket_send_msg(ctx->sock, buf, buf_len, + WEBSOCKET_OPCODE_DATA_TEXT, + true, true, timeout); + if (ret < 0) { + errno = -ret; + return -1; + } + + return ret; +} + +static int websocket_recv(struct websocket_context *ctx, u8_t *buf, + size_t buf_len, s32_t timeout) +{ + u32_t message_type; + u64_t remaining; + int ret; + + NET_DBG("[%p] Waiting data, buf len %zd bytes", ctx, buf_len); + + /* TODO: add support for recvmsg() so that we could return the + * websocket specific information in ancillary data. + */ + ret = websocket_recv_msg(ctx->sock, buf, buf_len, &message_type, + &remaining, timeout); + if (ret < 0) { + errno = -ret; + return -1; + } + + return ret; +} + +static ssize_t websocket_read_vmeth(void *obj, void *buffer, size_t count) +{ + return (ssize_t)websocket_recv(obj, buffer, count, K_FOREVER); +} + +static ssize_t websocket_write_vmeth(void *obj, const void *buffer, + size_t count) +{ + return (ssize_t)websocket_send(obj, buffer, count, K_FOREVER); +} + +static ssize_t websocket_sendto_ctx(void *obj, const void *buf, size_t len, + int flags, + const struct sockaddr *dest_addr, + socklen_t addrlen) +{ + struct websocket_context *ctx = obj; + s32_t timeout = K_FOREVER; + + if ((flags & ZSOCK_MSG_DONTWAIT)) { + timeout = K_NO_WAIT; + } + + ARG_UNUSED(dest_addr); + ARG_UNUSED(addrlen); + + return (ssize_t)websocket_send(ctx, buf, len, timeout); +} + +static ssize_t websocket_recvfrom_ctx(void *obj, void *buf, size_t max_len, + int flags, struct sockaddr *src_addr, + socklen_t *addrlen) +{ + struct websocket_context *ctx = obj; + s32_t timeout = K_FOREVER; + + if ((flags & ZSOCK_MSG_DONTWAIT)) { + timeout = K_NO_WAIT; + } + + ARG_UNUSED(src_addr); + ARG_UNUSED(addrlen); + + return (ssize_t)websocket_recv(ctx, buf, max_len, timeout); +} + +static const struct socket_op_vtable websocket_fd_op_vtable = { + .fd_vtable = { + .read = websocket_read_vmeth, + .write = websocket_write_vmeth, + .ioctl = websocket_ioctl_vmeth, + }, + .sendto = websocket_sendto_ctx, + .recvfrom = websocket_recvfrom_ctx, +}; + +void websocket_context_foreach(websocket_context_cb_t cb, void *user_data) +{ + int i; + + k_sem_take(&contexts_lock, K_FOREVER); + + for (i = 0; i < ARRAY_SIZE(contexts); i++) { + if (!websocket_context_is_used(&contexts[i])) { + continue; + } + + k_mutex_lock(&contexts[i].lock, K_FOREVER); + + cb(&contexts[i], user_data); + + k_mutex_unlock(&contexts[i].lock); + } + + k_sem_give(&contexts_lock); +} + +void websocket_init(void) +{ + k_sem_init(&contexts_lock, 1, UINT_MAX); +} diff --git a/subsys/net/lib/websocket/websocket_internal.h b/subsys/net/lib/websocket/websocket_internal.h new file mode 100644 index 0000000000000..a72ed023ebe7c --- /dev/null +++ b/subsys/net/lib/websocket/websocket_internal.h @@ -0,0 +1,167 @@ +/** @file + @brief Websocket private header + + This is not to be included by the application. + */ + +/* + * Copyright (c) 2019 Intel Corporation + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#define WS_SHA1_OUTPUT_LEN 20 + +/* Min Websocket header length */ +#define MIN_HEADER_LEN 2 + +/* Max Websocket header length */ +#define MAX_HEADER_LEN 14 + +/* From RFC 6455 chapter 4.2.2 */ +#define WS_MAGIC "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +/** + * Websocket connection information + */ +struct websocket_context { + union { + /** User data. + */ + void *user_data; + + /** This is used during HTTP handshake to verify that the + * peer sent proper Sec-WebSocket-Accept key. + */ + u8_t *sec_accept_key; + }; + + /** Reference count. + */ + atomic_t refcount; + + /** Internal lock for protecting this context from multiple access. + */ + struct k_mutex lock; + + /* The socket number is valid only after HTTP handshake is done + * so we can share the memory for these. + */ + union { + /** HTTP parser settings for the application usage */ + const struct http_parser_settings *http_cb; + + /** The Websocket socket id. If data is sent via this socket, it + * will automatically add Websocket headers etc into the data. + */ + int sock; + }; + + /** Temporary buffers used for HTTP handshakes and Websocket protocol + * headers. User must provide the actual buffer where the headers are + * stored temporarily. + */ + u8_t *tmp_buf; + + /** Temporary buffer length. + */ + size_t tmp_buf_len; + + /** The real TCP socket to use when sending Websocket data to peer. + */ + int real_sock; + + /** Websocket connection masking value */ + u32_t masking_value; + + /** Timeout for Websocket operations. + */ + s32_t timeout; + + /** Internal buffer for Websocket header when reading data. + */ + struct { + u8_t header[MAX_HEADER_LEN]; + u8_t pos; + }; + + /** Amount of data received. */ + u64_t total_read; + + /** Message length */ + u64_t message_len; + + /** Message type */ + u32_t message_type; + + /** Is the message masked */ + u8_t masked:1; + + /** Did we receive Sec-WebSocket-Accept: field */ + u8_t sec_accept_present:1; + + /** Is Sec-WebSocket-Accept field correct */ + u8_t sec_accept_ok:1; + + /** Did we receive all from peer during HTTP handshake */ + u8_t all_received:1; + + /** Header received */ + u8_t header_received:1; +}; + +/** + * @brief Disconnect the Websocket. + * + * @param sock Websocket id returned by websocket_connect() call. + * + * @return 0 if ok, <0 if error + */ +int websocket_disconnect(int sock); + +#if 0 +/** + * @brief Send data to Websocket. + * + * @param ctx Websocket context. + * @param buf Buffer to send. + * @param buf_len Amount of data to send. + * @param timeout Timeout for the data to send. + * + * @return <0 if error, >=0 amount of data sent + */ +int websocket_send(struct websocket_context *ctx, u8_t *buf, size_t buf_len, + s32_t timeout); + +/** + * @brief Receive data from Websocket. + * + * @param ctx Websocket context. + * @param buf Buffer to receive data. + * @param buf_len Size of the buffer. + * @param timeout Timeout for the data to send. + * + * @return <0 if error, >=0 amount of data received + */ +int websocket_recv(struct websocket_context *ctx, u8_t *buf, size_t buf_len, + s32_t timeout); +#endif + +/** + * @typedef websocket_context_cb_t + * @brief Callback used while iterating over websocket contexts + * + * @param context A valid pointer on current websocket context + * @param user_data A valid pointer on some user data or NULL + */ +typedef void (*websocket_context_cb_t)(struct websocket_context *ctx, + void *user_data); + +/** + * @brief Iterate over websocket context. This is mainly used by net-shell + * to show information about websockets. + * + * @param cb Websocket context callback + * @param user_data Caller specific data. + */ +void websocket_context_foreach(websocket_context_cb_t cb, void *user_data);