samples: net: http_client: HTTP client sample application
Simple HTTP client sample that connects to HTTP server and does
GET and POST requests.
Signed-off-by: Jukka Rissanen <jukka.rissanen@linux.intel.com>
diff --git a/samples/net/sockets/http_client/CMakeLists.txt b/samples/net/sockets/http_client/CMakeLists.txt
new file mode 100644
index 0000000..5aba16b
--- /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 0000000..4d1459f
--- /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: <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/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 <https://github.com/zephyrproject-rtos/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 0000000..1a91e13
--- /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 0000000..968b5bb
--- /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 0000000..610ad75
--- /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 0000000..4e55d41
--- /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 0000000..fac8c0c
--- /dev/null
+++ b/samples/net/sockets/http_client/src/https-cert.der
Binary files 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 0000000..2a9db58
--- /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 <logging/log.h>
+LOG_MODULE_REGISTER(net_http_client_sample, LOG_LEVEL_DBG);
+
+#include <net/net_ip.h>
+#include <net/socket.h>
+#include <net/tls_credentials.h>
+#include <net/http_client.h>
+
+#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);
+}