diff --git a/samples/net/sockets/http_client/CMakeLists.txt b/samples/net/sockets/http_client/CMakeLists.txt new file mode 100644 index 00000000000..5aba16b3b5a --- /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/https-cert.der + ${gen_dir}/https-cert.der.inc + ) diff --git a/samples/net/sockets/http_client/README.rst b/samples/net/sockets/http_client/README.rst new file mode 100644 index 00000000000..4d1459f8a8f --- /dev/null +++ b/samples/net/sockets/http_client/README.rst @@ -0,0 +1,88 @@ +.. _sockets-http-client-sample: + +Socket HTTP Client +################## + +Overview +******** + +This sample application implements an HTTP(S) client that will do an HTTP +or HTTPS request and wait for the response from the HTTP server. + +The source code for this sample application can be found at: +:zephyr_file:`samples/net/sockets/http_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 the http-client sample application like this: + +.. zephyr-app-commands:: + :zephyr-app: samples/net/sockets/http_client + :board: + :conf: + :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/http_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/http_client/src/` directory. +The default certificates used by Socket HTTP Client and +``https-server.py`` program found in the +`net-tools `_ project, enable +establishing a secure connection between the samples. + + +Running http-server in Linux Host +================================= + +You can run this ``http-client`` sample application in QEMU +and run the ``http-server.py`` (from net-tools) on a Linux host. + +To use QEMU for testing, follow the :ref:`networking_with_qemu` guide. + +In a terminal window: + +.. code-block:: console + + $ ./http-server.py + +Run ``http-client`` application in QEMU: + +.. zephyr-app-commands:: + :zephyr-app: samples/net/sockets/http_client + :host-os: unix + :board: qemu_x86 + :conf: prj.conf + :goals: run + :compact: + +Note that ``http-server.py`` must be running in the Linux host terminal window +before you start the http-client application in QEMU. +Exit QEMU by pressing :kbd:`CTRL+A` :kbd:`x`. + +You can verify TLS communication with a Linux host as well. Just use the +``https-server.py`` program in net-tools project. diff --git a/samples/net/sockets/http_client/overlay-tls.conf b/samples/net/sockets/http_client/overlay-tls.conf new file mode 100644 index 00000000000..1a91e13e3d6 --- /dev/null +++ b/samples/net/sockets/http_client/overlay-tls.conf @@ -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 diff --git a/samples/net/sockets/http_client/prj.conf b/samples/net/sockets/http_client/prj.conf new file mode 100644 index 00000000000..968b5bbb8cd --- /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 00000000000..610ad75e29a --- /dev/null +++ b/samples/net/sockets/http_client/sample.yaml @@ -0,0 +1,11 @@ +common: + tags: net http http_client + min_ram: 32 + # Blacklist qemu_x86_64 because of SSE compile error, see #19066 for details + platform_exclude: qemu_x86_64 +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 00000000000..4e55d4198cc --- /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/https-cert.pem file + */ +static const unsigned char ca_certificate[] = { +#include "https-cert.der.inc" +}; diff --git a/samples/net/sockets/http_client/src/https-cert.der b/samples/net/sockets/http_client/src/https-cert.der new file mode 100644 index 00000000000..fac8c0cbcdd Binary files /dev/null and b/samples/net/sockets/http_client/src/https-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 00000000000..2a9db583395 --- /dev/null +++ b/samples/net/sockets/http_client/src/main.c @@ -0,0 +1,357 @@ +/* + * 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 +#define HTTPS_PORT 4443 + +#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", + "chunked", + "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 void 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); + LOG_INF("Response status %s", rsp->http_status); +} + +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_SECONDS(3); + int ret; + int port = HTTP_PORT; + + 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); + exit(1); + } + + port = HTTPS_PORT; + } + + 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)) { + struct http_request req; + + memset(&req, 0, sizeof(req)); + + req.method = HTTP_GET; + req.url = "/"; + req.host = SERVER_ADDR4; + req.protocol = "HTTP/1.1"; + req.response = response_cb; + req.recv_buf = recv_buf_ipv4; + req.recv_buf_len = sizeof(recv_buf_ipv4); + + ret = http_client_req(sock4, &req, 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"; + req.response = response_cb; + req.recv_buf = recv_buf_ipv6; + req.recv_buf_len = sizeof(recv_buf_ipv6); + + ret = http_client_req(sock6, &req, timeout, "IPv6 GET"); + + close(sock6); + } + + sock4 = -1; + sock6 = -1; + + 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)) { + 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); + req.response = response_cb; + req.recv_buf = recv_buf_ipv4; + req.recv_buf_len = sizeof(recv_buf_ipv4); + + ret = http_client_req(sock4, &req, 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); + req.response = response_cb; + req.recv_buf = recv_buf_ipv6; + req.recv_buf_len = sizeof(recv_buf_ipv6); + + ret = http_client_req(sock6, &req, timeout, "IPv6 POST"); + + close(sock6); + } + + /* Do a chunked POST request */ + + sock4 = -1; + sock6 = -1; + + 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)) { + struct http_request req; + const char *headers[] = { + "Transfer-Encoding: chunked\r\n", + NULL + }; + + memset(&req, 0, sizeof(req)); + + req.method = HTTP_POST; + req.url = "/chunked-test"; + req.host = SERVER_ADDR4; + req.protocol = "HTTP/1.1"; + req.payload_cb = payload_cb; + req.header_fields = headers; + req.response = response_cb; + req.recv_buf = recv_buf_ipv4; + req.recv_buf_len = sizeof(recv_buf_ipv4); + + ret = http_client_req(sock4, &req, 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 = "/chunked-test"; + req.host = SERVER_ADDR6; + req.protocol = "HTTP/1.1"; + req.payload_cb = payload_cb; + req.header_fields = headers; + req.response = response_cb; + req.recv_buf = recv_buf_ipv6; + req.recv_buf_len = sizeof(recv_buf_ipv6); + + ret = http_client_req(sock6, &req, timeout, "IPv6 POST"); + + close(sock6); + } + + k_sleep(K_FOREVER); +}