diff --git a/HTTPServer/.gitignore b/HTTPServer/.gitignore new file mode 100644 index 0000000..5c1f8f1 --- /dev/null +++ b/HTTPServer/.gitignore @@ -0,0 +1,4 @@ +BUILD/ +mbed-http/ +mbed-os/ +rapidjson/ diff --git a/HTTPServer/.mbedignore b/HTTPServer/.mbedignore new file mode 100644 index 0000000..8530a26 --- /dev/null +++ b/HTTPServer/.mbedignore @@ -0,0 +1,3 @@ +rapidjson/test/* +rapidjson/example/* +rapidjson/include/rapidjson/msinttypes/* diff --git a/HTTPServer/README.md b/HTTPServer/README.md new file mode 100644 index 0000000..01995ee --- /dev/null +++ b/HTTPServer/README.md @@ -0,0 +1,20 @@ +# mbed-os-example-http-server + +This application demonstrates how to run an HTTP server on an mbed OS 5 device. + +Request parsing is done through [nodejs/http-parser](https://github.com/nodejs/http-parser). + +## To build + +1. Open ``mbed_app.json`` and change the `network-default-interface-type` option to your connectivity method ([more info](https://os.mbed.com/docs/development/apis/network-interfaces.html) +2. Build the project in the online compiler or using mbed CLI. +3. Flash the project to your development board. +4. Attach a serial monitor to your board to see the debug messages. + +## Tested on + +* K64F with Ethernet. +* NUCLEO_F411RE with ESP8266. + * For ESP8266, you need [this patch](https://github.com/ARMmbed/esp8266-driver/pull/41). + +But every networking stack that supports the [mbed OS 5 NetworkInterface API](https://docs.mbed.com/docs/mbed-os-api-reference/en/latest/APIs/communication/network_sockets/) should work. diff --git a/HTTPServer/http_response_builder.cpp b/HTTPServer/http_response_builder.cpp new file mode 100644 index 0000000..3dcb3c6 --- /dev/null +++ b/HTTPServer/http_response_builder.cpp @@ -0,0 +1,232 @@ +/* + * PackageLicenseDeclared: Apache-2.0 + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "mbed.h" +#include "http_response_builder.h" + +static const char *get_http_status_string(uint16_t status_code) +{ + switch (status_code) { + case 100: + return "Continue"; + case 101: + return "Switching Protocols"; + case 102: + return "Processing"; + case 200: + return "OK"; + case 201: + return "Created"; + case 202: + return "Accepted"; + case 203: + return "Non-Authoritative Information"; + case 204: + return "No Content"; + case 205: + return "Reset Content"; + case 206: + return "Partial Content"; + case 207: + return "Multi-Status"; + case 208: + return "Already Reported"; + case 226: + return "IM Used"; + case 300: + return "Multiple Choices"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 303: + return "See Other"; + case 304: + return "Not Modified"; + case 305: + return "Use Proxy"; + case 307: + return "Temporary Redirect"; + case 308: + return "Permanent Redirect"; + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + case 402: + return "Payment Required"; + case 403: + return "Forbidden"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + case 406: + return "Not Acceptable"; + case 407: + return "Proxy Authentication Required"; + case 408: + return "Request Timeout"; + case 409: + return "Conflict"; + case 410: + return "Gone"; + case 411: + return "Length Required"; + case 412: + return "Precondition Failed"; + case 413: + return "Payload Too Large"; + case 414: + return "URI Too Long"; + case 415: + return "Unsupported Media Type"; + case 416: + return "Range Not Satisfiable"; + case 417: + return "Expectation Failed"; + case 421: + return "Misdirected Request"; + case 422: + return "Unprocessable Entity"; + case 423: + return "Locked"; + case 424: + return "Failed Dependency"; + case 426: + return "Upgrade Required"; + case 428: + return "Precondition Required"; + case 429: + return "Too Many Requests"; + case 431: + return "Request Header Fields Too Large"; + case 451: + return "Unavailable For Legal Reasons"; + case 500: + return "Internal Server Error"; + case 501: + return "Not Implemented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + case 505: + return "HTTP Version Not Supported"; + case 506: + return "Variant Also Negotiates"; + case 507: + return "Insufficient Storage"; + case 508: + return "Loop Detected"; + case 510: + return "Not Extended"; + case 511: + return "Network Authentication Required"; + default : + return "Unknown"; + } +} + +HttpResponseBuilder::HttpResponseBuilder(uint16_t a_status_code) + : status_code(a_status_code), status_message(get_http_status_string(a_status_code)) +{ + set_header("Content-Type", "text/plain"); +} + +void HttpResponseBuilder::set_header(string key, string value) +{ + map::iterator it = headers.find(key); + + if (it != headers.end()) { + it->second = value; + } else { + headers.insert(headers.end(), pair(key, value)); + } +} + +char *HttpResponseBuilder::build(const void *body, size_t body_size, size_t *size) +{ + char buffer[10]; + snprintf(buffer, sizeof(buffer), "%d", body_size); + set_header("Content-Length", string(buffer)); + + char status_code_buffer[5]; + snprintf(status_code_buffer, sizeof(status_code_buffer), "%d", status_code /* max 5 digits */); + + *size = 0; + + // first line is HTTP/1.1 200 OK\r\n + *size += 8 + 1 + strlen(status_code_buffer) + 1 + strlen(status_message) + 2; + + // after that we'll do the headers + typedef map::iterator it_type; + for (it_type it = headers.begin(); it != headers.end(); it++) { + // line is KEY: VALUE\r\n + *size += it->first.length() + 1 + 1 + it->second.length() + 2; + } + + // then the body, first an extra newline + *size += 2; + + // body + *size += body_size; + + // Now let's print it + char *res = (char *)calloc(*size + 1, 1); + char *originalRes = res; + + res += sprintf(res, "HTTP/1.1 %s %s\r\n", status_code_buffer, status_message); + + typedef map::iterator it_type; + for (it_type it = headers.begin(); it != headers.end(); it++) { + // line is KEY: VALUE\r\n + res += sprintf(res, "%s: %s\r\n", it->first.c_str(), it->second.c_str()); + } + + res += sprintf(res, "\r\n"); + + if (body_size > 0) { + memcpy(res, body, body_size); + } + res += body_size; + + // Uncomment to debug... + // printf("----- BEGIN RESPONSE -----\n"); + // printf("%s", originalRes); + // printf("----- END RESPONSE -----\n"); + + return originalRes; +} + +nsapi_error_t HttpResponseBuilder::send(TCPSocket *socket, const void *body, size_t body_size) +{ + if (!socket) { + return NSAPI_ERROR_NO_SOCKET; + } + + size_t res_size; + char *response = build(body, body_size, &res_size); + + nsapi_error_t r = socket->send(response, res_size); + + free(response); + + return r; +} diff --git a/HTTPServer/http_response_builder.h b/HTTPServer/http_response_builder.h new file mode 100644 index 0000000..e3f0a07 --- /dev/null +++ b/HTTPServer/http_response_builder.h @@ -0,0 +1,44 @@ +/* + * PackageLicenseDeclared: Apache-2.0 + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _MBED_HTTP_RESPONSE_BUILDER_ +#define _MBED_HTTP_RESPONSE_BUILDER_ + +#include +#include +#include "http_parser.h" +#include "http_parsed_url.h" + +class HttpResponseBuilder { +public: + HttpResponseBuilder(uint16_t a_status_code); + + /** + * Set a header for the request + * If the key already exists, it will be overwritten... + */ + void set_header(string key, string value); + char *build(const void *body, size_t body_size, size_t *size); + nsapi_error_t send(TCPSocket *socket, const void *body, size_t body_size); + +private: + uint16_t status_code; + const char *status_message; + map headers; +}; + +#endif // _MBED_HTTP_RESPONSE_BUILDER_ diff --git a/HTTPServer/http_server.cpp b/HTTPServer/http_server.cpp new file mode 100644 index 0000000..bd632e2 --- /dev/null +++ b/HTTPServer/http_server.cpp @@ -0,0 +1,190 @@ +/* + * PackageLicenseDeclared: Apache-2.0 + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "http_server.h" + +HttpServer::HttpServer(NetworkInterface *network) : _network(network), server_thread(osPriorityNormal, 2048, NULL, "server_thread") +{ + _network = network; +} + +HttpServer::~HttpServer() +{ + for (size_t ix = 0; ix < HTTP_SERVER_MAX_CONCURRENT; ix++) { + if (socket_threads[ix]) { + delete socket_threads[ix]; + } + } +} + +nsapi_error_t HttpServer::start(uint16_t port, Callback a_handler) +{ + server = new TCPSocket(); + + nsapi_error_t ret; + + ret = server->open(_network); + if (ret != NSAPI_ERROR_OK) { + return ret; + } + + ret = server->bind(port); + if (ret != NSAPI_ERROR_OK) { + return ret; + } + + ret = server->listen(HTTP_SERVER_MAX_CONCURRENT); + if (ret != NSAPI_ERROR_OK) { + return ret; + } + + handler = a_handler; + + server_thread.start(callback(this, &HttpServer::server_loop)); + + return NSAPI_ERROR_OK; +} + +void HttpServer::receive_data() +{ + TCPSocket *socket = sockets.back(); + + // needs to keep running until the socket gets closed + while (1) { + + ParsedHttpRequest *response = new ParsedHttpRequest(); + HttpParser *parser = new HttpParser(response, HTTP_REQUEST); + + // Set up a receive buffer (on the heap) + uint8_t *recv_buffer = (uint8_t *)malloc(HTTP_RECEIVE_BUFFER_SIZE); + + // TCPSocket::recv is called until we don't have any data anymore + nsapi_size_or_error_t recv_ret; + while ((recv_ret = socket->recv(recv_buffer, HTTP_RECEIVE_BUFFER_SIZE)) > 0) { + // Pass the chunk into the http_parser + int32_t nparsed = parser->execute((const char *)recv_buffer, recv_ret); + if (nparsed != recv_ret) { + printf("Parsing failed... parsed %ld bytes, received %d bytes\n", nparsed, recv_ret); + recv_ret = -2101; + break; + } + + if (response->is_message_complete()) { + break; + } + } + + if (recv_ret <= 0) { + if (recv_ret < 0) { + printf("Error reading from socket %d\n", recv_ret); + } + + // error = recv_ret; + delete response; + delete parser; + free(recv_buffer); + + return; + } + + // When done, call parser.finish() + parser->finish(); + + // Free the receive buffer + free(recv_buffer); + + // Let user application handle the request, if user needs a handle to response they need to memcpy themselves + if (recv_ret > 0) { + handler(response, socket); + } + + // Free the response and parser + delete response; + delete parser; + } +} + +void HttpServer::server_loop() +{ + while (1) { + TCPSocket *clt_sock = server->accept(); + if (clt_sock) { + log_server_state(); + + sockets.push_back(clt_sock); // so we can clear our disconnected sockets + + // and start listening for events there + Thread *t = new Thread(osPriorityNormal, 2048, NULL, NULL); + t->start(callback(this, &HttpServer::receive_data)); + + socket_thread_metadata_t *m = new socket_thread_metadata_t(); + m->socket = clt_sock; + m->thread = t; + socket_threads.push_back(m); + } else { + delete clt_sock; + } + + for (size_t ix = 0; ix < socket_threads.size(); ix++) { + if ((uint32_t)osThreadGetState(socket_threads[ix]->thread->get_id()) == osThreadTerminated) { + printf("Thread Deleted\r\n"); + socket_threads[ix]->thread->terminate(); + socket_threads[ix]->socket->close(); + delete socket_threads[ix]->thread; + socket_threads.erase(socket_threads.begin() + ix); + } + } + + } +} + +void HttpServer::log_server_state(void) +{ + uint8_t count = SocketStats::mbed_stats_socket_get_each(&stats[0], MBED_CONF_NSAPI_SOCKET_STATS_MAX_COUNT); + printf("%-15s%-15s%-15s%-15s%-15s%-15s\n", "ID", "State", "Proto", "Sent", "Recv", "Time"); + + for (int i = 0; i < count; i++) { + printf("%-15p", stats[i].reference_id); + + switch (stats[i].state) { + case SOCK_CLOSED: + printf("%-15s", "Closed"); + break; + case SOCK_OPEN: + printf("%-15s", "Open"); + break; + case SOCK_CONNECTED: + printf("%-15s", "Connected"); + break; + case SOCK_LISTEN: + printf("%-15s", "Listen"); + break; + default: + printf("%-15s", "Error"); + break; + } + + if (NSAPI_TCP == stats[i].proto) { + printf("%-15s", "TCP"); + } else { + printf("%-15s", "UDP"); + } + printf("%-15d", stats[i].sent_bytes); + printf("%-15d", stats[i].recv_bytes); + printf("%-15lld\n", stats[i].last_change_tick); + } +} diff --git a/HTTPServer/http_server.h b/HTTPServer/http_server.h new file mode 100644 index 0000000..f6f22a5 --- /dev/null +++ b/HTTPServer/http_server.h @@ -0,0 +1,73 @@ +/* + * PackageLicenseDeclared: Apache-2.0 + * Copyright (c) 2017 ARM Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _HTTP_SERVER_ +#define _HTTP_SERVER_ + +#include "mbed.h" +#include "http_request_parser.h" +#include "http_response.h" +#include "http_response_builder.h" + +#ifndef HTTP_SERVER_MAX_CONCURRENT +#define HTTP_SERVER_MAX_CONCURRENT 5 +#endif + +typedef HttpResponse ParsedHttpRequest; + +/** + * \brief HttpServer implements the logic for setting up an HTTP server. + */ +class HttpServer { +public: + /** + * HttpRequest Constructor + * + * @param[in] network The network interface + */ + HttpServer(NetworkInterface *network); + + ~HttpServer(); + + /** + * Start running the server (it will run on it's own thread) + */ + nsapi_error_t start(uint16_t port, Callback a_handler); + +private: + + void receive_data(); + + void server_loop(); + + void log_server_state(void); + + typedef struct { + TCPSocket *socket; + Thread *thread; + } socket_thread_metadata_t; + + TCPSocket *server; + NetworkInterface *_network; + Thread server_thread; + vector sockets; + vector socket_threads; + Callback handler; + mbed_stats_socket_t stats[MBED_CONF_NSAPI_SOCKET_STATS_MAX_COUNT]; +}; + +#endif // _HTTP_SERVER diff --git a/HTTPServer/main.cpp b/HTTPServer/main.cpp new file mode 100644 index 0000000..c7785e4 --- /dev/null +++ b/HTTPServer/main.cpp @@ -0,0 +1,100 @@ +/* mbed Microcontroller Library + * Copyright (c) 2019 ARM Limited + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "mbed.h" +#include "http_server.h" +#include "http_response_builder.h" +#include "webpage.h" +#include "rapidjson/document.h" +#include "rapidjson/writer.h" +#include "rapidjson/stringbuffer.h" + +using namespace rapidjson; + +DigitalOut led(LED1); +bool stats_enabled = false; + +static uint8_t max_thread_count = 16; +mbed_stats_thread_t *thread_stats = new mbed_stats_thread_t[max_thread_count]; + +// Requests come in here +void request_handler(ParsedHttpRequest *request, TCPSocket *socket) +{ + printf("Request came in: %s %s\n", http_method_str(request->get_method()), request->get_url().c_str()); + + if (request->get_method() == HTTP_GET && request->get_url() == "/") { + HttpResponseBuilder builder(200); + builder.set_header("Content-Type", "text/html; charset=utf-8"); + + builder.send(socket, response, sizeof(response) - 1); + } else if (request->get_method() == HTTP_POST && request->get_url() == "/toggle") { + printf("toggle LED called\n"); + led = !led; + + HttpResponseBuilder builder(200); + builder.send(socket, NULL, 0); + } else if (request->get_method() == HTTP_GET && request->get_url() == "/api/stats") { + // create JSON object with information about the thread statistics + rapidjson::Document document; + document.SetArray(); + + int count = mbed_stats_thread_get_each(thread_stats, max_thread_count); + for (int i = 0; i < count; i++) { + Value thread_info(kObjectType); + Value thread_name; + thread_name.SetString(thread_stats[i].name, strlen(thread_stats[i].name), document.GetAllocator()); + + thread_info.AddMember("name", thread_name, document.GetAllocator()); + thread_info.AddMember("state", static_cast(thread_stats[i].state), document.GetAllocator()); + thread_info.AddMember("stack_space", static_cast(thread_stats[i].stack_space), document.GetAllocator()); + document.PushBack(thread_info, document.GetAllocator()); + } + + StringBuffer strbuf; + Writer writer(strbuf); + document.Accept(writer); + + HttpResponseBuilder builder(200); + builder.set_header("Content-Type", "application/json"); + builder.send(socket, strbuf.GetString(), strbuf.GetSize()); + } else { + char resp[512]; + int resp_length = snprintf(resp, sizeof(resp), "Page not found: '%s'", request->get_url().c_str()); + + HttpResponseBuilder builder(404); + builder.send(socket, resp, resp_length); + } +} + +int main() +{ + printf("Connecting to network...\r\n"); + + // Connect to the network with the default network interface + // If you use a shield or an external module you need to replace this line + // and load the driver yourself. See https://os.mbed.com/docs/mbed-os/v5.11/reference/ip-networking.html + NetworkInterface *network = NetworkInterface::get_default_instance(); + if (!network) { + printf("Could not find default network instance\n"); + return 1; + } + + nsapi_error_t connect_status = network->connect(); + if (connect_status != NSAPI_ERROR_OK) { + printf("Could not connect to network\n"); + return 1; + } + + HttpServer server(network); + nsapi_error_t res = server.start(8080, &request_handler); + + if (res == NSAPI_ERROR_OK) { + printf("Server is listening at http://%s:8080\n", network->get_ip_address()); + } else { + printf("Server could not be started... %d\n", res); + } + + wait(osWaitForever); +} diff --git a/HTTPServer/mbed-http.lib b/HTTPServer/mbed-http.lib new file mode 100644 index 0000000..ffd565d --- /dev/null +++ b/HTTPServer/mbed-http.lib @@ -0,0 +1 @@ +https://developer.mbed.org/teams/sandbox/code/mbed-http/#6daf67a96a91 diff --git a/HTTPServer/mbed-os.lib b/HTTPServer/mbed-os.lib new file mode 100644 index 0000000..efedfa8 --- /dev/null +++ b/HTTPServer/mbed-os.lib @@ -0,0 +1 @@ +https://github.com/ARMmbed/mbed-os/#51d55508e8400b60af467005646c4e2164738d48 diff --git a/HTTPServer/mbed_app.json b/HTTPServer/mbed_app.json new file mode 100644 index 0000000..f1c6f83 --- /dev/null +++ b/HTTPServer/mbed_app.json @@ -0,0 +1,14 @@ +{ + "target_overrides": { + "*": { + "mbed-http.http-buffer-size": 2048, + "platform.stdio-baud-rate": 115200, + "platform.stdio-convert-newlines": true, + "platform.thread-stats-enabled": 1, + "nsapi.socket-stats-enable": 1, + "nsapi.default-wifi-security": "WPA_WPA2", + "nsapi.default-wifi-ssid": "\"SSID\"", + "nsapi.default-wifi-password": "\"Password\"" + } + } +} diff --git a/HTTPServer/rapidjson.lib b/HTTPServer/rapidjson.lib new file mode 100644 index 0000000..eab01de --- /dev/null +++ b/HTTPServer/rapidjson.lib @@ -0,0 +1 @@ +https://github.com/Tencent/rapidjson/#7484e06c589873e1ed80382d262087e4fa80fb63 diff --git a/HTTPServer/webpage.h b/HTTPServer/webpage.h new file mode 100644 index 0000000..42a3969 --- /dev/null +++ b/HTTPServer/webpage.h @@ -0,0 +1,47 @@ +const char response[] = + "\r\n" + "\r\n" + "\r\n" + " Hello from Mbed OS\r\n" + " \r\n" + "\r\n" + "\r\n" + "

Mbed OS Webserver

\r\n" + "\r\n" + "\r\n" + "
\r\n" + "\r\n" + "\r\n" + "\r\n" + ;