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);
+}