From 8d5de2c85d9446c2e68124d4f0bf5234b6fcdf36 Mon Sep 17 00:00:00 2001 From: Hugo Lefeuvre Date: Thu, 19 Sep 2024 19:32:12 -0700 Subject: [PATCH] Add an HTTP server example to showcase the new TCP server API. Signed-off-by: Hugo Lefeuvre --- examples/05.HTTP_SERVER/README.md | 6 + examples/05.HTTP_SERVER/http_server.cc | 209 +++++++++++++++++++++++++ examples/05.HTTP_SERVER/xmake.lua | 62 ++++++++ 3 files changed, 277 insertions(+) create mode 100644 examples/05.HTTP_SERVER/README.md create mode 100644 examples/05.HTTP_SERVER/http_server.cc create mode 100644 examples/05.HTTP_SERVER/xmake.lua diff --git a/examples/05.HTTP_SERVER/README.md b/examples/05.HTTP_SERVER/README.md new file mode 100644 index 0000000..f930c99 --- /dev/null +++ b/examples/05.HTTP_SERVER/README.md @@ -0,0 +1,6 @@ +HTTP Server Example +=================== + +This example shows simple use of the TCP socket server API. +It opens server port 80 and serves a static HTTP page there. +Note that this is *not* intended as an example of how to build an HTTP server. diff --git a/examples/05.HTTP_SERVER/http_server.cc b/examples/05.HTTP_SERVER/http_server.cc new file mode 100644 index 0000000..94e44b8 --- /dev/null +++ b/examples/05.HTTP_SERVER/http_server.cc @@ -0,0 +1,209 @@ +#include "timeout.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using CHERI::Capability; + +using Debug = ConditionalDebug; +constexpr bool UseIPv6 = CHERIOT_RTOS_OPTION_IPv6; + +#define LISTEN_PORT 80 +DECLARE_AND_DEFINE_BIND_CAPABILITY(HTTPPort, UseIPv6, LISTEN_PORT); + +DECLARE_AND_DEFINE_ALLOCATOR_CAPABILITY(TestMalloc, 32 * 1024); +#define TEST_MALLOC STATIC_SEALED_VALUE(TestMalloc) + +static char reply[] = + "HTTP/1.1 200 OK\r\n" + "Content-type: text/html\r\n" + "Connection: close\r\n" + "\r\n" + "" + "" + "Hello from CHERIoT!" + "

It works!

Served from a CHERIoT device.

" + "\n"; + +/** + * Maximum number of clients the server will server before shutting down. This + * is useful to check that the server can handle multiple clients before + * terminating. + */ +static const uint16_t MaxClients = 10; + +/** + * This example server is written to showcase the network stack server API, but + * also the network stack restart. If one of the network stack APIs fails in a + * way likely caused by a crash, the server will wait this small delay before + * retrying to give time to the network stack to reset. + */ +static const uint16_t RestartDelay = 100; // in ticks + +void __cheri_compartment("http_server_example") example() +{ + network_start(); + + auto heapAtStart = heap_quota_remaining(TEST_MALLOC); + + uint16_t clientsCounter = 0; + + Debug::log("Starting the server."); + + while (clientsCounter < MaxClients) + { + Debug::log("Creating a listening socket."); + Timeout unlimited{UnlimitedTimeout}; + auto socket = network_socket_listen_tcp( + &unlimited, TEST_MALLOC, STATIC_SEALED_VALUE(HTTPPort)); + + if (!Capability{socket}.is_valid()) + { + Debug::log("Failed to create a listening socket."); + // This may have failed because of a network stack + // crash. Sleep a little bit to enable a reset. + Timeout sleep{RestartDelay}; + thread_sleep(&sleep); + continue; + } + + Debug::log("Listening on port {}...", LISTEN_PORT); + while (clientsCounter < MaxClients) + { + NetworkAddress clientAddress = {0}; + uint16_t clientPort = 0; + + auto clientSocket = network_socket_accept_tcp( + &unlimited, TEST_MALLOC, socket, &clientAddress, &clientPort); + + if (!Capability{clientSocket}.is_valid()) + { + Debug::log("Failed to establish a connection."); + Timeout sleep{RestartDelay}; + thread_sleep(&sleep); + break; + } + + if (!UseIPv6) + { + Debug::log("Established a connection with {}.{}.{}.{}:{}", + static_cast(clientAddress.ipv4) & 0xff, + static_cast(clientAddress.ipv4 >> 8) & 0xff, + static_cast(clientAddress.ipv4 >> 16) & 0xff, + static_cast(clientAddress.ipv4 >> 24) & 0xff, + clientPort); + } + else + { + Debug::log("Established a connection."); + } + + clientsCounter++; + + auto [received, buffer] = + network_socket_receive(&unlimited, TEST_MALLOC, clientSocket); + + // For this simple server, we do not care about what the client + // sent (we will always serve the same content). + int ret = heap_free(TEST_MALLOC, buffer); + if (ret != 0) + { + // This may happen if the network stack crashed. + Debug::log("Failed to free receive buffer: {}", ret); + // Do not break here - we still want to free the socket. + } + + if (received > 0) + { + Debug::log( + "Received {} bytes from the client, serving static content.", + received); + constexpr size_t toSend = sizeof(reply) - 1; + size_t sent = 0; + while (sent < toSend) + { + size_t remaining = toSend - sent; + + size_t sentThisCall = network_socket_send( + &unlimited, clientSocket, &(reply[sent]), remaining); + Debug::log("Sent {} bytes", sentThisCall); + + if (sentThisCall >= 0) + { + sent += sentThisCall; + } + else + { + Debug::log("Send failed: {}", sentThisCall); + break; + } + } + } + else + { + Debug::log( + "Failed to receive request from the client, error {}.", + received); + } + + Debug::log("Terminating the connection with the client."); + int retries = 10; + // In a retry loop to be more rebust to network stack crashes. + for (; retries > 0; retries--) + { + if (network_socket_close( + &unlimited, TEST_MALLOC, clientSocket) == 0) + { + break; + } + Timeout sleep{RestartDelay}; + thread_sleep(&sleep); + } + + if (retries == 0) + { + Debug::log("Failed to close the client socket."); + } + } + + Debug::log("Closing the listening socket."); + int retries = 10; + // In a retry loop to be more rebust to network stack crashes. + for (; retries > 0; retries--) + { + if (network_socket_close(&unlimited, TEST_MALLOC, socket) == 0) + { + break; + } + Timeout sleep{RestartDelay}; + thread_sleep(&sleep); + } + + if (retries == 0) + { + Debug::log("Failed to close the listening socket."); + } + } + + Debug::log("Now checking for leaks."); + auto heapAtEnd = heap_quota_remaining(TEST_MALLOC); + if (heapAtEnd < heapAtStart) + { + Debug::log("Warning: The implementation leaked {} bytes (start: {} vs. " + "end: {}).", + heapAtStart - heapAtEnd, + heapAtEnd, + heapAtStart); + } + else + { + Debug::log("No leaks detected."); + } + + Debug::log("Terminating the server."); +} diff --git a/examples/05.HTTP_SERVER/xmake.lua b/examples/05.HTTP_SERVER/xmake.lua new file mode 100644 index 0000000..f2fddcb --- /dev/null +++ b/examples/05.HTTP_SERVER/xmake.lua @@ -0,0 +1,62 @@ +-- Copyright CHERIoT Contributors. +-- SPDX-License-Identifier: MIT + +-- Update this to point to the location of the CHERIoT SDK +sdkdir = path.absolute("../../../cheriot-rtos/sdk") + +set_project("CHERIoT HTTP Server Example") + +includes(sdkdir) + +set_toolchains("cheriot-clang") + +includes(path.join(sdkdir, "lib")) +includes("../../lib") + +option("board") + set_default("ibex-arty-a7-100") + +compartment("http_server_example") + set_default(false) + add_includedirs("../../include") + add_deps("freestanding", "TCPIP", "NetAPI") + add_files("http_server.cc") + on_load(function(target) + target:add('options', "IPv6") + local IPv6 = get_config("IPv6") + target:add("defines", "CHERIOT_RTOS_OPTION_IPv6=" .. tostring(IPv6)) + end) + +firmware("05.http_server_example") + set_policy("build.warning", true) + add_deps("TCPIP", "Firewall", "NetAPI", "http_server_example", "atomic8", "debug") + on_load(function(target) + target:values_set("board", "$(board)") + target:values_set("threads", { + { + compartment = "http_server_example", + priority = 1, + entry_point = "example", + stack_size = 0xe00, + trusted_stack_frames = 6 + }, + { + compartment = "TCPIP", + priority = 1, + entry_point = "ip_thread_entry", + stack_size = 0xe00, + trusted_stack_frames = 5 + }, + { + compartment = "Firewall", + -- Higher priority, this will be back-pressured by the message + -- queue if the network stack can't keep up, but we want + -- packets to arrive immediately. + priority = 2, + entry_point = "ethernet_run_driver", + stack_size = 0x1000, + trusted_stack_frames = 5 + } + }, {expand = false}) + end) +