samples: net: websocket: App for doing Websocket client requests

This is BSD sockets based application for connecting to
Websocket server.

Signed-off-by: Jukka Rissanen <jukka.rissanen@linux.intel.com>
This commit is contained in:
Jukka Rissanen 2019-07-31 11:30:50 +03:00
commit b031a5a163
8 changed files with 608 additions and 0 deletions

View file

@ -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/https-cert.der
${gen_dir}/https-cert.der.inc
)

View file

@ -0,0 +1,94 @@
.. _sockets-websocket-client-sample:
Socket Websocket Client
#######################
Overview
********
This sample application implements a Websocket client that will do an HTTP
or HTTPS handshake request to HTTP server, then start to send data and wait for
the responses from the Websocket server.
The source code for this sample application can be found at:
:zephyr_file:`samples/net/sockets/websocket_client`.
Requirements
************
- :ref:`networking_with_host`
Building and Running
********************
You can use this application on a supported board, including
running it inside QEMU as described in :ref:`networking_with_qemu`.
Build websocket-client sample application like this:
.. zephyr-app-commands::
:zephyr-app: samples/net/sockets/websocket_client
:board: <board to use>
:conf: <config file to use>
:goals: build
:compact:
Enabling TLS support
====================
Enable TLS support in the sample by building the project with the
``overlay-tls.conf`` overlay file enabled using these commands:
.. zephyr-app-commands::
:zephyr-app: samples/net/sockets/websocket_client
:board: qemu_x86
:conf: "prj.conf overlay-tls.conf"
:goals: build
:compact:
An alternative way is to specify ``-DOVERLAY_CONFIG=overlay-tls.conf`` when
running ``west build`` or ``cmake``.
The certificate and private key used by the sample can be found in the sample's
:zephyr_file:`samples/net/sockets/websocket_client/src/` directory.
Running websocket-server in Linux Host
======================================
You can run this ``websocket-client`` sample application in QEMU
and run the ``zephyr-websocket-server.py`` (from net-tools) on a Linux host.
Other alternative is to install `websocketd <http://websocketd.com/>`_ and
use that.
To use QEMU for testing, follow the :ref:`networking_with_qemu` guide.
In a terminal window you can do either:
.. code-block:: console
$ ./zephyr-websocket-server.py
or
.. code-block:: console
$ websocketd --port=9001 cat
Run ``websocket-client`` application in QEMU:
.. zephyr-app-commands::
:zephyr-app: samples/net/sockets/websocket_client
:host-os: unix
:board: qemu_x86
:conf: prj.conf
:goals: run
:compact:
Note that ``zephyr-websocket-server.py`` or ``websocketd`` must be running in
the Linux host terminal window before you start the ``websocket-client``
application in QEMU. Exit QEMU by pressing :kbd:`CTRL+A` :kbd:`x`.
Current version of ``zephyr-websocket-server.py`` found in
`net-tools <https://github.com/zephyrproject-rtos/net-tools>`_ project, does
not support TLS.

View file

@ -0,0 +1,13 @@
CONFIG_MAIN_STACK_SIZE=3072
CONFIG_NET_BUF_RX_COUNT=80
CONFIG_NET_BUF_TX_COUNT=80
# TLS configuration
CONFIG_MBEDTLS=y
CONFIG_MBEDTLS_BUILTIN=y
CONFIG_MBEDTLS_ENABLE_HEAP=y
CONFIG_MBEDTLS_HEAP_SIZE=60000
CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=2048
CONFIG_NET_SOCKETS_SOCKOPT_TLS=y
CONFIG_NET_SOCKETS_TLS_MAX_CONTEXTS=6

View file

@ -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

View file

@ -0,0 +1,11 @@
common:
tags: net http http_client websocket_client websocket
min_ram: 35
# Blacklist qemu_x86_64 because of SSE compile error, see #19066 for details
platform_exclude: qemu_x86_64
sample:
description: Websocket client sample
name: websocket_client
tests:
sample.net.sockets.websocket_client:
harness: net

View file

@ -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/https-cert.pem file
*/
static const unsigned char ca_certificate[] = {
#include "https-cert.der.inc"
};

View file

@ -0,0 +1,415 @@
/*
* Copyright (c) 2019 Intel Corporation
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <logging/log.h>
LOG_MODULE_REGISTER(net_websocket_client_sample, LOG_LEVEL_DBG);
#include <net/net_ip.h>
#include <net/socket.h>
#include <net/tls_credentials.h>
#include <net/websocket.h>
#include <shell/shell.h>
#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."
" "
"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];
/* We need to allocate bigger buffer for the websocket data we receive so that
* the websocket header fits into it.
*/
#define EXTRA_BUF_SPACE 30
static u8_t temp_recv_buf_ipv4[MAX_RECV_BUF_LEN + EXTRA_BUF_SPACE];
static u8_t temp_recv_buf_ipv6[MAX_RECV_BUF_LEN + EXTRA_BUF_SPACE];
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;
int remaining;
int ret, read_pos;
remaining = amount - 1;
read_pos = 0;
while (remaining > 0) {
ret = websocket_recv_msg(sock, buf + read_pos,
buf_len - read_pos,
&message_type,
NULL,
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;
remaining -= ret;
}
if (remaining <= 0 &&
memcmp(lorem_ipsum, buf, amount - 1) != 0) {
LOG_ERR("%s data recv failure %zd bytes", proto, amount - 1);
}
}
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 - 1;
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 - 1) != 0) {
LOG_ERR("%s data recv failure %zd bytes", proto, amount - 1);
}
}
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;
}
/* Terminate the sent data with \n so that we can use the
* websocketd --port=9001 cat
* command in server side.
*/
memcpy(buf, lorem_ipsum, amount);
buf[amount] = '\n';
/* Send every 2nd 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, buf, amount);
} else {
ret = sendall_with_bsd_api(sock, buf, 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)
{
/* Just an example how to set extra headers */
const char *extra_headers[] = {
"Origin: http://foobar\r\n",
NULL
};
int sock4 = -1, sock6 = -1;
int websock4 = -1, websock6 = -1;
s32_t timeout = K_SECONDS(3);
struct sockaddr_in6 addr6;
struct sockaddr_in addr4;
size_t amount;
int ret;
if (IS_ENABLED(CONFIG_NET_SOCKETS_SOCKOPT_TLS)) {
ret = tls_credential_add(CA_CERTIFICATE_TAG,
TLS_CREDENTIAL_CA_CERTIFICATE,
ca_certificate,
sizeof(ca_certificate));
if (ret < 0) {
LOG_ERR("Failed to register public certificate: %d",
ret);
k_sleep(K_FOREVER);
}
}
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.");
k_sleep(K_FOREVER);
}
if (sock4 >= 0 && IS_ENABLED(CONFIG_NET_IPV4)) {
struct websocket_request req;
memset(&req, 0, sizeof(req));
req.host = SERVER_ADDR4;
req.url = "/";
req.optional_headers = extra_headers;
req.cb = connect_cb;
req.tmp_buf = temp_recv_buf_ipv4;
req.tmp_buf_len = sizeof(temp_recv_buf_ipv4);
websock4 = websocket_connect(sock4, &req, 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)) {
struct websocket_request req;
memset(&req, 0, sizeof(req));
req.host = SERVER_ADDR6;
req.url = "/";
req.optional_headers = extra_headers;
req.cb = connect_cb;
req.tmp_buf = temp_recv_buf_ipv6;
req.tmp_buf_len = sizeof(temp_recv_buf_ipv6);
websock6 = websocket_connect(sock6, &req, 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");
k_sleep(K_FOREVER);
}
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);
}
k_sleep(K_FOREVER);
}