diff --git a/components/esp_http_server/CMakeLists.txt b/components/esp_http_server/CMakeLists.txt new file mode 100644 index 00000000..700a0b9c --- /dev/null +++ b/components/esp_http_server/CMakeLists.txt @@ -0,0 +1,13 @@ +set(COMPONENT_ADD_INCLUDEDIRS include) +set(COMPONENT_PRIV_INCLUDEDIRS src/port/esp32 src/util) +set(COMPONENT_SRCS "src/httpd_main.c" + "src/httpd_parse.c" + "src/httpd_sess.c" + "src/httpd_txrx.c" + "src/httpd_uri.c" + "src/util/ctrl_sock.c") + +set(COMPONENT_REQUIRES nghttp) # for http_parser.h +set(COMPONENT_PRIV_REQUIRES lwip) + +register_component() diff --git a/components/esp_http_server/Kconfig b/components/esp_http_server/Kconfig new file mode 100644 index 00000000..323961ad --- /dev/null +++ b/components/esp_http_server/Kconfig @@ -0,0 +1,15 @@ +menu "HTTP Server" + +config HTTPD_MAX_REQ_HDR_LEN + int "Max HTTP Request Header Length" + default 512 + help + This sets the maximum supported size of headers section in HTTP request packet to be processed by the server + +config HTTPD_MAX_URI_LEN + int "Max HTTP URI Length" + default 512 + help + This sets the maximum supported size of HTTP request URI to be processed by the server + +endmenu diff --git a/components/esp_http_server/component.mk b/components/esp_http_server/component.mk new file mode 100644 index 00000000..e34ca073 --- /dev/null +++ b/components/esp_http_server/component.mk @@ -0,0 +1,4 @@ + +COMPONENT_SRCDIRS := src src/util +COMPONENT_ADD_INCLUDEDIRS := include +COMPONENT_PRIV_INCLUDEDIRS := src/port/esp32 src/util diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h new file mode 100644 index 00000000..793f3dd8 --- /dev/null +++ b/components/esp_http_server/include/esp_http_server.h @@ -0,0 +1,1188 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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 _ESP_HTTP_SERVER_H_ +#define _ESP_HTTP_SERVER_H_ + +#include <stdio.h> +#include <string.h> +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> +#include <http_parser.h> +#include <sdkconfig.h> +#include <esp_err.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/* +note: esp_https_server.h includes a customized copy of this +initializer that should be kept in sync +*/ +#define HTTPD_DEFAULT_CONFIG() { \ + .task_priority = tskIDLE_PRIORITY+5, \ + .stack_size = 4096, \ + .server_port = 80, \ + .ctrl_port = 32768, \ + .max_open_sockets = 7, \ + .max_uri_handlers = 8, \ + .max_resp_headers = 8, \ + .backlog_conn = 5, \ + .lru_purge_enable = false, \ + .recv_wait_timeout = 5, \ + .send_wait_timeout = 5, \ + .global_user_ctx = NULL, \ + .global_user_ctx_free_fn = NULL, \ + .global_transport_ctx = NULL, \ + .global_transport_ctx_free_fn = NULL, \ + .open_fn = NULL, \ + .close_fn = NULL, \ +} + +#define ESP_ERR_HTTPD_BASE (0x8000) /*!< Starting number of HTTPD error codes */ +#define ESP_ERR_HTTPD_HANDLERS_FULL (ESP_ERR_HTTPD_BASE + 1) /*!< All slots for registering URI handlers have been consumed */ +#define ESP_ERR_HTTPD_HANDLER_EXISTS (ESP_ERR_HTTPD_BASE + 2) /*!< URI handler with same method and target URI already registered */ +#define ESP_ERR_HTTPD_INVALID_REQ (ESP_ERR_HTTPD_BASE + 3) /*!< Invalid request pointer */ +#define ESP_ERR_HTTPD_RESULT_TRUNC (ESP_ERR_HTTPD_BASE + 4) /*!< Result string truncated */ +#define ESP_ERR_HTTPD_RESP_HDR (ESP_ERR_HTTPD_BASE + 5) /*!< Response header field larger than supported */ +#define ESP_ERR_HTTPD_RESP_SEND (ESP_ERR_HTTPD_BASE + 6) /*!< Error occured while sending response packet */ +#define ESP_ERR_HTTPD_ALLOC_MEM (ESP_ERR_HTTPD_BASE + 7) /*!< Failed to dynamically allocate memory for resource */ +#define ESP_ERR_HTTPD_TASK (ESP_ERR_HTTPD_BASE + 8) /*!< Failed to launch server task/thread */ + +/* ************** Group: Initialization ************** */ +/** @name Initialization + * APIs related to the Initialization of the web server + * @{ + */ + +/** + * @brief HTTP Server Instance Handle + * + * Every instance of the server will have a unique handle. + */ +typedef void* httpd_handle_t; + +/** + * @brief HTTP Method Type wrapper over "enum http_method" + * available in "http_parser" library + */ +typedef enum http_method httpd_method_t; + +/** + * @brief Prototype for freeing context data (if any) + * @param[in] ctx : object to free + */ +typedef void (*httpd_free_ctx_fn_t)(void *ctx); + +/** + * @brief Function prototype for opening a session. + * + * Called immediately after the socket was opened to set up the send/recv functions and + * other parameters of the socket. + * + * @param[in] hd : server instance + * @param[in] sockfd : session socket file descriptor + * @return status + */ +typedef esp_err_t (*httpd_open_func_t)(httpd_handle_t hd, int sockfd); + +/** + * @brief Function prototype for closing a session. + * + * @note It's possible that the socket descriptor is invalid at this point, the function + * is called for all terminated sessions. Ensure proper handling of return codes. + * + * @param[in] hd : server instance + * @param[in] sockfd : session socket file descriptor + */ +typedef void (*httpd_close_func_t)(httpd_handle_t hd, int sockfd); + +/** + * @brief HTTP Server Configuration Structure + * + * @note Use HTTPD_DEFAULT_CONFIG() to initialize the configuration + * to a default value and then modify only those fields that are + * specifically determined by the use case. + */ +typedef struct httpd_config { + unsigned task_priority; /*!< Priority of FreeRTOS task which runs the server */ + size_t stack_size; /*!< The maximum stack size allowed for the server task */ + + /** + * TCP Port number for receiving and transmitting HTTP traffic + */ + uint16_t server_port; + + /** + * UDP Port number for asynchronously exchanging control signals + * between various components of the server + */ + uint16_t ctrl_port; + + uint16_t max_open_sockets; /*!< Max number of sockets/clients connected at any time*/ + uint16_t max_uri_handlers; /*!< Maximum allowed uri handlers */ + uint16_t max_resp_headers; /*!< Maximum allowed additional headers in HTTP response */ + uint16_t backlog_conn; /*!< Number of backlog connections */ + bool lru_purge_enable; /*!< Purge "Least Recently Used" connection */ + uint16_t recv_wait_timeout; /*!< Timeout for recv function (in seconds)*/ + uint16_t send_wait_timeout; /*!< Timeout for send function (in seconds)*/ + + /** + * Global user context. + * + * This field can be used to store arbitrary user data within the server context. + * The value can be retrieved using the server handle, available e.g. in the httpd_req_t struct. + * + * When shutting down, the server frees up the user context by + * calling free() on the global_user_ctx field. If you wish to use a custom + * function for freeing the global user context, please specify that here. + */ + void * global_user_ctx; + + /** + * Free function for global user context + */ + httpd_free_ctx_fn_t global_user_ctx_free_fn; + + /** + * Global transport context. + * + * Similar to global_user_ctx, but used for session encoding or encryption (e.g. to hold the SSL context). + * It will be freed using free(), unless global_transport_ctx_free_fn is specified. + */ + void * global_transport_ctx; + + /** + * Free function for global transport context + */ + httpd_free_ctx_fn_t global_transport_ctx_free_fn; + + /** + * Custom session opening callback. + * + * Called on a new session socket just after accept(), but before reading any data. + * + * This is an opportunity to set up e.g. SSL encryption using global_transport_ctx + * and the send/recv/pending session overrides. + * + * If a context needs to be maintained between these functions, store it in the session using + * httpd_sess_set_transport_ctx() and retrieve it later with httpd_sess_get_transport_ctx() + */ + httpd_open_func_t open_fn; + + /** + * Custom session closing callback. + * + * Called when a session is deleted, before freeing user and transport contexts and before + * closing the socket. This is a place for custom de-init code common to all sockets. + * + * Set the user or transport context to NULL if it was freed here, so the server does not + * try to free it again. + * + * This function is run for all terminated sessions, including sessions where the socket + * was closed by the network stack - that is, the file descriptor may not be valid anymore. + */ + httpd_close_func_t close_fn; +} httpd_config_t; + +/** + * @brief Starts the web server + * + * Create an instance of HTTP server and allocate memory/resources for it + * depending upon the specified configuration. + * + * Example usage: + * @code{c} + * + * //Function for starting the webserver + * httpd_handle_t start_webserver(void) + * { + * // Generate default configuration + * httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + * + * // Empty handle to http_server + * httpd_handle_t server = NULL; + * + * // Start the httpd server + * if (httpd_start(&server, &config) == ESP_OK) { + * // Register URI handlers + * httpd_register_uri_handler(server, &uri_get); + * httpd_register_uri_handler(server, &uri_post); + * } + * // If server failed to start, handle will be NULL + * return server; + * } + * + * @endcode + * + * @param[in] config : Configuration for new instance of the server + * @param[out] handle : Handle to newly created instance of the server. NULL on error + * @return + * - ESP_OK : Instance created successfully + * - ESP_ERR_INVALID_ARG : Null argument(s) + * - ESP_ERR_HTTPD_ALLOC_MEM : Failed to allocate memory for instance + * - ESP_ERR_HTTPD_TASK : Failed to launch server task + */ +esp_err_t httpd_start(httpd_handle_t *handle, const httpd_config_t *config); + +/** + * @brief Stops the web server + * + * Deallocates memory/resources used by an HTTP server instance and + * deletes it. Once deleted the handle can no longer be used for accessing + * the instance. + * + * Example usage: + * @code{c} + * + * // Function for stopping the webserver + * void stop_webserver(httpd_handle_t server) + * { + * // Ensure handle is non NULL + * if (server != NULL) { + * // Stop the httpd server + * httpd_stop(server); + * } + * } + * + * @endcode + * + * @param[in] handle Handle to server returned by httpd_start + * @return + * - ESP_OK : Server stopped successfully + * - ESP_ERR_INVALID_ARG : Handle argument is Null + */ +esp_err_t httpd_stop(httpd_handle_t handle); + +/** End of Group Initialization + * @} + */ + +/* ************** Group: URI Handlers ************** */ +/** @name URI Handlers + * APIs related to the URI handlers + * @{ + */ + +/* Max supported HTTP request header length */ +#define HTTPD_MAX_REQ_HDR_LEN CONFIG_HTTPD_MAX_REQ_HDR_LEN + +/* Max supported HTTP request URI length */ +#define HTTPD_MAX_URI_LEN CONFIG_HTTPD_MAX_URI_LEN + +/** + * @brief HTTP Request Data Structure + */ +typedef struct httpd_req { + httpd_handle_t handle; /*!< Handle to server instance */ + int method; /*!< The type of HTTP request, -1 if unsupported method */ + const char uri[HTTPD_MAX_URI_LEN + 1]; /*!< The URI of this request (1 byte extra for null termination) */ + size_t content_len; /*!< Length of the request body */ + void *aux; /*!< Internally used members */ + + /** + * User context pointer passed during URI registration. + */ + void *user_ctx; + + /** + * Session Context Pointer + * + * A session context. Contexts are maintained across 'sessions' for a + * given open TCP connection. One session could have multiple request + * responses. The web server will ensure that the context persists + * across all these request and responses. + * + * By default, this is NULL. URI Handlers can set this to any meaningful + * value. + * + * If the underlying socket gets closed, and this pointer is non-NULL, + * the web server will free up the context by calling free(), unless + * free_ctx function is set. + */ + void *sess_ctx; + + /** + * Pointer to free context hook + * + * Function to free session context + * + * If the web server's socket closes, it frees up the session context by + * calling free() on the sess_ctx member. If you wish to use a custom + * function for freeing the session context, please specify that here. + */ + httpd_free_ctx_fn_t free_ctx; +} httpd_req_t; + +/** + * @brief Structure for URI handler + */ +typedef struct httpd_uri { + const char *uri; /*!< The URI to handle */ + httpd_method_t method; /*!< Method supported by the URI */ + + /** + * Handler to call for supported request method. This must + * return ESP_OK, or else the underlying socket will be closed. + */ + esp_err_t (*handler)(httpd_req_t *r); + + /** + * Pointer to user context data which will be available to handler + */ + void *user_ctx; +} httpd_uri_t; + +/** + * @brief Registers a URI handler + * + * @note URI handlers can be registered in real time as long as the + * server handle is valid. + * + * Example usage: + * @code{c} + * + * esp_err_t my_uri_handler(httpd_req_t* req) + * { + * // Recv , Process and Send + * .... + * .... + * .... + * + * // Fail condition + * if (....) { + * // Return fail to close session // + * return ESP_FAIL; + * } + * + * // On success + * return ESP_OK; + * } + * + * // URI handler structure + * httpd_uri_t my_uri { + * .uri = "/my_uri/path/xyz", + * .method = HTTPD_GET, + * .handler = my_uri_handler, + * .user_ctx = NULL + * }; + * + * // Register handler + * if (httpd_register_uri_handler(server_handle, &my_uri) != ESP_OK) { + * // If failed to register handler + * .... + * } + * + * @endcode + * + * @param[in] handle handle to HTTPD server instance + * @param[in] uri_handler pointer to handler that needs to be registered + * + * @return + * - ESP_OK : On successfully registering the handler + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_HTTPD_HANDLERS_FULL : If no slots left for new handler + * - ESP_ERR_HTTPD_HANDLER_EXISTS : If handler with same URI and + * method is already registered + */ +esp_err_t httpd_register_uri_handler(httpd_handle_t handle, + const httpd_uri_t *uri_handler); + +/** + * @brief Unregister a URI handler + * + * @param[in] handle handle to HTTPD server instance + * @param[in] uri URI string + * @param[in] method HTTP method + * + * @return + * - ESP_OK : On successfully deregistering the handler + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_NOT_FOUND : Handler with specified URI and method not found + */ +esp_err_t httpd_unregister_uri_handler(httpd_handle_t handle, + const char *uri, httpd_method_t method); + +/** + * @brief Unregister all URI handlers with the specified uri string + * + * @param[in] handle handle to HTTPD server instance + * @param[in] uri uri string specifying all handlers that need + * to be deregisterd + * + * @return + * - ESP_OK : On successfully deregistering all such handlers + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_NOT_FOUND : No handler registered with specified uri string + */ +esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char* uri); + +/** End of URI Handlers + * @} + */ + +/* ************** Group: TX/RX ************** */ +/** @name TX / RX + * Prototype for HTTPDs low-level send/recv functions + * @{ + */ + +#define HTTPD_SOCK_ERR_FAIL -1 +#define HTTPD_SOCK_ERR_INVALID -2 +#define HTTPD_SOCK_ERR_TIMEOUT -3 + +/** + * @brief Prototype for HTTPDs low-level send function + * + * @note User specified send function must handle errors internally, + * depending upon the set value of errno, and return specific + * HTTPD_SOCK_ERR_ codes, which will eventually be conveyed as + * return value of httpd_send() function + * + * @param[in] hd : server instance + * @param[in] sockfd : session socket file descriptor + * @param[in] buf : buffer with bytes to send + * @param[in] buf_len : data size + * @param[in] flags : flags for the send() function + * @return + * - Bytes : The number of bytes sent successfully + * - HTTPD_SOCK_ERR_INVALID : Invalid arguments + * - HTTPD_SOCK_ERR_TIMEOUT : Timeout/interrupted while calling socket send() + * - HTTPD_SOCK_ERR_FAIL : Unrecoverable error while calling socket send() + */ +typedef int (*httpd_send_func_t)(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags); + +/** + * @brief Prototype for HTTPDs low-level recv function + * + * @note User specified recv function must handle errors internally, + * depending upon the set value of errno, and return specific + * HTTPD_SOCK_ERR_ codes, which will eventually be conveyed as + * return value of httpd_req_recv() function + * + * @param[in] hd : server instance + * @param[in] sockfd : session socket file descriptor + * @param[in] buf : buffer with bytes to send + * @param[in] buf_len : data size + * @param[in] flags : flags for the send() function + * @return + * - Bytes : The number of bytes received successfully + * - 0 : Buffer length parameter is zero / connection closed by peer + * - HTTPD_SOCK_ERR_INVALID : Invalid arguments + * - HTTPD_SOCK_ERR_TIMEOUT : Timeout/interrupted while calling socket recv() + * - HTTPD_SOCK_ERR_FAIL : Unrecoverable error while calling socket recv() + */ +typedef int (*httpd_recv_func_t)(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, int flags); + +/** + * @brief Prototype for HTTPDs low-level "get pending bytes" function + * + * @note User specified pending function must handle errors internally, + * depending upon the set value of errno, and return specific + * HTTPD_SOCK_ERR_ codes, which will be handled accordingly in + * the server task. + * + * @param[in] hd : server instance + * @param[in] sockfd : session socket file descriptor + * @return + * - Bytes : The number of bytes waiting to be received + * - HTTPD_SOCK_ERR_INVALID : Invalid arguments + * - HTTPD_SOCK_ERR_TIMEOUT : Timeout/interrupted while calling socket pending() + * - HTTPD_SOCK_ERR_FAIL : Unrecoverable error while calling socket pending() + */ +typedef int (*httpd_pending_func_t)(httpd_handle_t hd, int sockfd); + +/** End of TX / RX + * @} + */ + +/* ************** Group: Request/Response ************** */ +/** @name Request / Response + * APIs related to the data send/receive by URI handlers. + * These APIs are supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * @{ + */ + +/** + * @brief Override web server's receive function (by session FD) + * + * This function overrides the web server's receive function. This same function is + * used to read HTTP request packets. + * + * @note This API is supposed to be called either from the context of + * - an http session APIs where sockfd is a valid parameter + * - a URI handler where sockfd is obtained using httpd_req_to_sockfd() + * + * @param[in] hd HTTPD instance handle + * @param[in] sockfd Session socket FD + * @param[in] recv_func The receive function to be set for this session + * + * @return + * - ESP_OK : On successfully registering override + * - ESP_ERR_INVALID_ARG : Null arguments + */ +esp_err_t httpd_sess_set_recv_override(httpd_handle_t hd, int sockfd, httpd_recv_func_t recv_func); + +/** + * @brief Override web server's send function (by session FD) + * + * This function overrides the web server's send function. This same function is + * used to send out any response to any HTTP request. + * + * @note This API is supposed to be called either from the context of + * - an http session APIs where sockfd is a valid parameter + * - a URI handler where sockfd is obtained using httpd_req_to_sockfd() + * + * @param[in] hd HTTPD instance handle + * @param[in] sockfd Session socket FD + * @param[in] send_func The send function to be set for this session + * + * @return + * - ESP_OK : On successfully registering override + * - ESP_ERR_INVALID_ARG : Null arguments + */ +esp_err_t httpd_sess_set_send_override(httpd_handle_t hd, int sockfd, httpd_send_func_t send_func); + +/** + * @brief Override web server's pending function (by session FD) + * + * This function overrides the web server's pending function. This function is + * used to test for pending bytes in a socket. + * + * @note This API is supposed to be called either from the context of + * - an http session APIs where sockfd is a valid parameter + * - a URI handler where sockfd is obtained using httpd_req_to_sockfd() + * + * @param[in] hd HTTPD instance handle + * @param[in] sockfd Session socket FD + * @param[in] pending_func The receive function to be set for this session + * + * @return + * - ESP_OK : On successfully registering override + * - ESP_ERR_INVALID_ARG : Null arguments + */ +esp_err_t httpd_sess_set_pending_override(httpd_handle_t hd, int sockfd, httpd_pending_func_t pending_func); + +/** + * @brief Get the Socket Descriptor from the HTTP request + * + * This API will return the socket descriptor of the session for + * which URI handler was executed on reception of HTTP request. + * This is useful when user wants to call functions that require + * session socket fd, from within a URI handler, ie. : + * httpd_sess_get_ctx(), + * httpd_sess_trigger_close(), + * httpd_sess_update_timestamp(). + * + * @note This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * + * @param[in] r The request whose socket descriptor should be found + * + * @return + * - Socket descriptor : The socket descriptor for this request + * - -1 : Invalid/NULL request pointer + */ +int httpd_req_to_sockfd(httpd_req_t *r); + +/** + * @brief API to read content data from the HTTP request + * + * This API will read HTTP content data from the HTTP request into + * provided buffer. Use content_len provided in httpd_req_t structure + * to know the length of data to be fetched. If content_len is too + * large for the buffer then user may have to make multiple calls to + * this function, each time fetching 'buf_len' number of bytes, + * while the pointer to content data is incremented internally by + * the same number. + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - If an error is returned, the URI handler must further return an error. + * This will ensure that the erroneous socket is closed and cleaned up by + * the web server. + * - Presently Chunked Encoding is not supported + * + * @param[in] r The request being responded to + * @param[in] buf Pointer to a buffer that the data will be read into + * @param[in] buf_len Length of the buffer + * + * @return + * - Bytes : Number of bytes read into the buffer successfully + * - 0 : Buffer length parameter is zero / connection closed by peer + * - HTTPD_SOCK_ERR_INVALID : Invalid arguments + * - HTTPD_SOCK_ERR_TIMEOUT : Timeout/interrupted while calling socket recv() + * - HTTPD_SOCK_ERR_FAIL : Unrecoverable error while calling socket recv() + */ +int httpd_req_recv(httpd_req_t *r, char *buf, size_t buf_len); + +/** + * @brief Search for a field in request headers and + * return the string length of it's value + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - Once httpd_resp_send() API is called all request headers + * are purged, so request headers need be copied into separate + * buffers if they are required later. + * + * @param[in] r The request being responded to + * @param[in] field The header field to be searched in the request + * + * @return + * - Length : If field is found in the request URL + * - Zero : Field not found / Invalid request / Null arguments + */ +size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *field); + +/** + * @brief Get the value string of a field from the request headers + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - Once httpd_resp_send() API is called all request headers + * are purged, so request headers need be copied into separate + * buffers if they are required later. + * - If output size is greater than input, then the value is truncated, + * accompanied by truncation error as return value. + * - Use httpd_req_get_hdr_value_len() to know the right buffer length + * + * @param[in] r The request being responded to + * @param[in] field The field to be searched in the header + * @param[out] val Pointer to the buffer into which the value will be copied if the field is found + * @param[in] val_size Size of the user buffer "val" + * + * @return + * - ESP_OK : Field found in the request header and value string copied + * - ESP_ERR_NOT_FOUND : Key not found + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_HTTPD_INVALID_REQ : Invalid HTTP request pointer + * - ESP_ERR_HTTPD_RESULT_TRUNC : Value string truncated + */ +esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *r, const char *field, char *val, size_t val_size); + +/** + * @brief Get Query string length from the request URL + * + * @note This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid + * + * @param[in] r The request being responded to + * + * @return + * - Length : Query is found in the request URL + * - Zero : Query not found / Null arguments / Invalid request + */ +size_t httpd_req_get_url_query_len(httpd_req_t *r); + +/** + * @brief Get Query string from the request URL + * + * @note + * - Presently, the user can fetch the full URL query string, but decoding + * will have to be performed by the user. Request headers can be read using + * httpd_req_get_hdr_value_str() to know the 'Content-Type' (eg. Content-Type: + * application/x-www-form-urlencoded) and then the appropriate decoding + * algorithm needs to be applied. + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid + * - If output size is greater than input, then the value is truncated, + * accompanied by truncation error as return value + * - Use httpd_req_get_url_query_len() to know the right buffer length + * + * @param[in] r The request being responded to + * @param[out] buf Pointer to the buffer into which the query string will be copied (if found) + * @param[in] buf_len Length of output buffer + * + * @return + * - ESP_OK : Query is found in the request URL and copied to buffer + * - ESP_ERR_NOT_FOUND : Query not found + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_HTTPD_INVALID_REQ : Invalid HTTP request pointer + * - ESP_ERR_HTTPD_RESULT_TRUNC : Query string truncated + */ +esp_err_t httpd_req_get_url_query_str(httpd_req_t *r, char *buf, size_t buf_len); + +/** + * @brief Helper function to get a URL query tag from a query + * string of the type param1=val1¶m2=val2 + * + * @note + * - The components of URL query string (keys and values) are not URLdecoded. + * The user must check for 'Content-Type' field in the request headers and + * then depending upon the specified encoding (URLencoded or otherwise) apply + * the appropriate decoding algorithm. + * - If actual value size is greater than val_size, then the value is truncated, + * accompanied by truncation error as return value. + * + * @param[in] qry Pointer to query string + * @param[in] key The key to be searched in the query string + * @param[out] val Pointer to the buffer into which the value will be copied if the key is found + * @param[in] val_size Size of the user buffer "val" + * + * @return + * - ESP_OK : Key is found in the URL query string and copied to buffer + * - ESP_ERR_NOT_FOUND : Key not found + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_HTTPD_RESULT_TRUNC : Value string truncated + */ +esp_err_t httpd_query_key_value(const char *qry, const char *key, char *val, size_t val_size); + +/** + * @brief API to send a complete HTTP response. + * + * This API will send the data as an HTTP response to the request. + * This assumes that you have the entire response ready in a single + * buffer. If you wish to send response in incremental chunks use + * httpd_resp_send_chunk() instead. + * + * If no status code and content-type were set, by default this + * will send 200 OK status code and content type as text/html. + * You may call the following functions before this API to configure + * the response headers : + * httpd_resp_set_status() - for setting the HTTP status string, + * httpd_resp_set_type() - for setting the Content Type, + * httpd_resp_set_hdr() - for appending any additional field + * value entries in the response header + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - Once this API is called, the request has been responded to. + * - No additional data can then be sent for the request. + * - Once this API is called, all request headers are purged, so + * request headers need be copied into separate buffers if + * they are required later. + * + * @param[in] r The request being responded to + * @param[in] buf Buffer from where the content is to be fetched + * @param[in] buf_len Length of the buffer, -1 to use strlen() + * + * @return + * - ESP_OK : On successfully sending the response packet + * - ESP_ERR_INVALID_ARG : Null request pointer + * - ESP_ERR_HTTPD_RESP_HDR : Essential headers are too large for internal buffer + * - ESP_ERR_HTTPD_RESP_SEND : Error in raw send + * - ESP_ERR_HTTPD_INVALID_REQ : Invalid request + */ +esp_err_t httpd_resp_send(httpd_req_t *r, const char *buf, ssize_t buf_len); + +/** + * @brief API to send one HTTP chunk + * + * This API will send the data as an HTTP response to the + * request. This API will use chunked-encoding and send the response + * in the form of chunks. If you have the entire response contained in + * a single buffer, please use httpd_resp_send() instead. + * + * If no status code and content-type were set, by default this will + * send 200 OK status code and content type as text/html. You may + * call the following functions before this API to configure the + * response headers + * httpd_resp_set_status() - for setting the HTTP status string, + * httpd_resp_set_type() - for setting the Content Type, + * httpd_resp_set_hdr() - for appending any additional field + * value entries in the response header + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - When you are finished sending all your chunks, you must call + * this function with buf_len as 0. + * - Once this API is called, all request headers are purged, so + * request headers need be copied into separate buffers if they + * are required later. + * + * @param[in] r The request being responded to + * @param[in] buf Pointer to a buffer that stores the data + * @param[in] buf_len Length of the data from the buffer that should be sent out, -1 to use strlen() + * + * @return + * - ESP_OK : On successfully sending the response packet chunk + * - ESP_ERR_INVALID_ARG : Null request pointer + * - ESP_ERR_HTTPD_RESP_HDR : Essential headers are too large for internal buffer + * - ESP_ERR_HTTPD_RESP_SEND : Error in raw send + * - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer + */ +esp_err_t httpd_resp_send_chunk(httpd_req_t *r, const char *buf, ssize_t buf_len); + +/* Some commonly used status codes */ +#define HTTPD_200 "200 OK" /*!< HTTP Response 200 */ +#define HTTPD_204 "204 No Content" /*!< HTTP Response 204 */ +#define HTTPD_207 "207 Multi-Status" /*!< HTTP Response 207 */ +#define HTTPD_400 "400 Bad Request" /*!< HTTP Response 400 */ +#define HTTPD_404 "404 Not Found" /*!< HTTP Response 404 */ +#define HTTPD_408 "408 Request Timeout" /*!< HTTP Response 408 */ +#define HTTPD_500 "500 Internal Server Error" /*!< HTTP Response 500 */ + +/** + * @brief API to set the HTTP status code + * + * This API sets the status of the HTTP response to the value specified. + * By default, the '200 OK' response is sent as the response. + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - This API only sets the status to this value. The status isn't + * sent out until any of the send APIs is executed. + * - Make sure that the lifetime of the status string is valid till + * send function is called. + * + * @param[in] r The request being responded to + * @param[in] status The HTTP status code of this response + * + * @return + * - ESP_OK : On success + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer + */ +esp_err_t httpd_resp_set_status(httpd_req_t *r, const char *status); + +/* Some commonly used content types */ +#define HTTPD_TYPE_JSON "application/json" /*!< HTTP Content type JSON */ +#define HTTPD_TYPE_TEXT "text/html" /*!< HTTP Content type text/HTML */ +#define HTTPD_TYPE_OCTET "application/octet-stream" /*!< HTTP Content type octext-stream */ + +/** + * @brief API to set the HTTP content type + * + * This API sets the 'Content Type' field of the response. + * The default content type is 'text/html'. + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - This API only sets the content type to this value. The type + * isn't sent out until any of the send APIs is executed. + * - Make sure that the lifetime of the type string is valid till + * send function is called. + * + * @param[in] r The request being responded to + * @param[in] type The Content Type of the response + * + * @return + * - ESP_OK : On success + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer + */ +esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *type); + +/** + * @brief API to append any additional headers + * + * This API sets any additional header fields that need to be sent in the response. + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - The header isn't sent out until any of the send APIs is executed. + * - The maximum allowed number of additional headers is limited to + * value of max_resp_headers in config structure. + * - Make sure that the lifetime of the field value strings are valid till + * send function is called. + * + * @param[in] r The request being responded to + * @param[in] field The field name of the HTTP header + * @param[in] value The value of this HTTP header + * + * @return + * - ESP_OK : On successfully appending new header + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_HTTPD_RESP_HDR : Total additional headers exceed max allowed + * - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer + */ +esp_err_t httpd_resp_set_hdr(httpd_req_t *r, const char *field, const char *value); + +/** + * @brief Helper function for HTTP 404 + * + * Send HTTP 404 message. If you wish to send additional data in the body of the + * response, please use the lower-level functions directly. + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - Once this API is called, all request headers are purged, so + * request headers need be copied into separate buffers if + * they are required later. + * + * @param[in] r The request being responded to + * + * @return + * - ESP_OK : On successfully sending the response packet + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_HTTPD_RESP_SEND : Error in raw send + * - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer + */ +esp_err_t httpd_resp_send_404(httpd_req_t *r); + +/** + * @brief Helper function for HTTP 408 + * + * Send HTTP 408 message. If you wish to send additional data in the body of the + * response, please use the lower-level functions directly. + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - Once this API is called, all request headers are purged, so + * request headers need be copied into separate buffers if + * they are required later. + * + * @param[in] r The request being responded to + * + * @return + * - ESP_OK : On successfully sending the response packet + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_HTTPD_RESP_SEND : Error in raw send + * - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer + */ +esp_err_t httpd_resp_send_408(httpd_req_t *r); + +/** + * @brief Helper function for HTTP 500 + * + * Send HTTP 500 message. If you wish to send additional data in the body of the + * response, please use the lower-level functions directly. + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - Once this API is called, all request headers are purged, so + * request headers need be copied into separate buffers if + * they are required later. + * + * @param[in] r The request being responded to + * + * @return + * - ESP_OK : On successfully sending the response packet + * - ESP_ERR_INVALID_ARG : Null arguments + * - ESP_ERR_HTTPD_RESP_SEND : Error in raw send + * - ESP_ERR_HTTPD_INVALID_REQ : Invalid request pointer + */ +esp_err_t httpd_resp_send_500(httpd_req_t *r); + +/** + * @brief Raw HTTP send + * + * Call this API if you wish to construct your custom response packet. + * When using this, all essential header, eg. HTTP version, Status Code, + * Content Type and Length, Encoding, etc. will have to be constructed + * manually, and HTTP delimeters (CRLF) will need to be placed correctly + * for separating sub-sections of the HTTP response packet. + * + * If the send override function is set, this API will end up + * calling that function eventually to send data out. + * + * @note + * - This API is supposed to be called only from the context of + * a URI handler where httpd_req_t* request pointer is valid. + * - Unless the response has the correct HTTP structure (which the + * user must now ensure) it is not guaranteed that it will be + * recognized by the client. For most cases, you wouldn't have + * to call this API, but you would rather use either of : + * httpd_resp_send(), + * httpd_resp_send_chunk() + * + * @param[in] r The request being responded to + * @param[in] buf Buffer from where the fully constructed packet is to be read + * @param[in] buf_len Length of the buffer + * + * @return + * - Bytes : Number of bytes that were sent successfully + * - HTTPD_SOCK_ERR_INVALID : Invalid arguments + * - HTTPD_SOCK_ERR_TIMEOUT : Timeout/interrupted while calling socket send() + * - HTTPD_SOCK_ERR_FAIL : Unrecoverable error while calling socket send() + */ +int httpd_send(httpd_req_t *r, const char *buf, size_t buf_len); + +/** End of Request / Response + * @} + */ + +/* ************** Group: Session ************** */ +/** @name Session + * Functions for controlling sessions and accessing context data + * @{ + */ + +/** + * @brief Get session context from socket descriptor + * + * Typically if a session context is created, it is available to URI handlers + * through the httpd_req_t structure. But, there are cases where the web + * server's send/receive functions may require the context (for example, for + * accessing keying information etc). Since the send/receive function only have + * the socket descriptor at their disposal, this API provides them with a way to + * retrieve the session context. + * + * @param[in] handle Handle to server returned by httpd_start + * @param[in] sockfd The socket descriptor for which the context should be extracted. + * + * @return + * - void* : Pointer to the context associated with this session + * - NULL : Empty context / Invalid handle / Invalid socket fd + */ +void *httpd_sess_get_ctx(httpd_handle_t handle, int sockfd); + +/** + * @brief Set session context by socket descriptor + * + * @param[in] handle Handle to server returned by httpd_start + * @param[in] sockfd The socket descriptor for which the context should be extracted. + * @param[in] ctx Context object to assign to the session + * @param[in] free_fn Function that should be called to free the context + */ +void httpd_sess_set_ctx(httpd_handle_t handle, int sockfd, void *ctx, httpd_free_ctx_fn_t free_fn); + +/** + * @brief Get session 'transport' context by socket descriptor + * @see httpd_sess_get_ctx() + * + * This context is used by the send/receive functions, for example to manage SSL context. + * + * @param[in] handle Handle to server returned by httpd_start + * @param[in] sockfd The socket descriptor for which the context should be extracted. + * @return + * - void* : Pointer to the transport context associated with this session + * - NULL : Empty context / Invalid handle / Invalid socket fd + */ +void *httpd_sess_get_transport_ctx(httpd_handle_t handle, int sockfd); + +/** + * @brief Set session 'transport' context by socket descriptor + * @see httpd_sess_set_ctx() + * + * @param[in] handle Handle to server returned by httpd_start + * @param[in] sockfd The socket descriptor for which the context should be extracted. + * @param[in] ctx Transport context object to assign to the session + * @param[in] free_fn Function that should be called to free the transport context + */ +void httpd_sess_set_transport_ctx(httpd_handle_t handle, int sockfd, void *ctx, httpd_free_ctx_fn_t free_fn); + +/** + * @brief Get HTTPD global user context (it was set in the server config struct) + * + * @param[in] handle Handle to server returned by httpd_start + * @return global user context + */ +void *httpd_get_global_user_ctx(httpd_handle_t handle); + +/** + * @brief Get HTTPD global transport context (it was set in the server config struct) + * + * @param[in] handle Handle to server returned by httpd_start + * @return global transport context + */ +void *httpd_get_global_transport_ctx(httpd_handle_t handle); + +/** + * @brief Trigger an httpd session close externally + * + * @note Calling this API is only required in special circumstances wherein + * some application requires to close an httpd client session asynchronously. + * + * @param[in] handle Handle to server returned by httpd_start + * @param[in] sockfd The socket descriptor of the session to be closed + * + * @return + * - ESP_OK : On successfully initiating closure + * - ESP_FAIL : Failure to queue work + * - ESP_ERR_NOT_FOUND : Socket fd not found + * - ESP_ERR_INVALID_ARG : Null arguments + */ +esp_err_t httpd_sess_trigger_close(httpd_handle_t handle, int sockfd); + +/** + * @brief Update timestamp for a given socket + * + * Timestamps are internally associated with each session to monitor + * how recently a session exchanged traffic. When LRU purge is enabled, + * if a client is requesting for connection but maximum number of + * sockets/sessions is reached, then the session having the earliest + * timestamp is closed automatically. + * + * Updating the timestamp manually prevents the socket from being purged + * due to the Least Recently Used (LRU) logic, even though it might not + * have received traffic for some time. This is useful when all open + * sockets/session are frequently exchanging traffic but the user specifically + * wants one of the sessions to be kept open, irrespective of when it last + * exchanged a packet. + * + * @note Calling this API is only necessary if the LRU Purge Enable option + * is enabled. + * + * @param[in] handle Handle to server returned by httpd_start + * @param[in] sockfd The socket descriptor of the session for which timestamp + * is to be updated + * + * @return + * - ESP_OK : Socket found and timestamp updated + * - ESP_ERR_NOT_FOUND : Socket not found + * - ESP_ERR_INVALID_ARG : Null arguments + */ +esp_err_t httpd_sess_update_timestamp(httpd_handle_t handle, int sockfd); + +/** End of Session + * @} + */ + +/* ************** Group: Work Queue ************** */ +/** @name Work Queue + * APIs related to the HTTPD Work Queue + * @{ + */ + +/** + * @brief Prototype of the HTTPD work function + * Please refer to httpd_queue_work() for more details. + * @param[in] arg The arguments for this work function + */ +typedef void (*httpd_work_fn_t)(void *arg); + +/** + * @brief Queue execution of a function in HTTPD's context + * + * This API queues a work function for asynchronous execution + * + * @note Some protocols require that the web server generate some asynchronous data + * and send it to the persistently opened connection. This facility is for use + * by such protocols. + * + * @param[in] handle Handle to server returned by httpd_start + * @param[in] work Pointer to the function to be executed in the HTTPD's context + * @param[in] arg Pointer to the arguments that should be passed to this function + * + * @return + * - ESP_OK : On successfully queueing the work + * - ESP_FAIL : Failure in ctrl socket + * - ESP_ERR_INVALID_ARG : Null arguments + */ +esp_err_t httpd_queue_work(httpd_handle_t handle, httpd_work_fn_t work, void *arg); + +/** End of Group Work Queue + * @} + */ + +#ifdef __cplusplus +} +#endif + +#endif /* ! _ESP_HTTP_SERVER_H_ */ diff --git a/components/esp_http_server/include/http_server.h b/components/esp_http_server/include/http_server.h new file mode 100644 index 00000000..56f73c5b --- /dev/null +++ b/components/esp_http_server/include/http_server.h @@ -0,0 +1,2 @@ +#warning http_server.h has been renamed to esp_http_server.h, please update include directives +#include "esp_http_server.h" diff --git a/components/esp_http_server/src/esp_httpd_priv.h b/components/esp_http_server/src/esp_httpd_priv.h new file mode 100644 index 00000000..68bc9d64 --- /dev/null +++ b/components/esp_http_server/src/esp_httpd_priv.h @@ -0,0 +1,531 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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 _HTTPD_PRIV_H_ +#define _HTTPD_PRIV_H_ + +#include <stdbool.h> +#include <sys/socket.h> +#include <sys/param.h> +#include <netinet/in.h> +#include <esp_log.h> +#include <esp_err.h> + +#include <esp_http_server.h> +#include "osal.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Size of request data block/chunk (not to be confused with chunked encoded data) + * that is received and parsed in one turn of the parsing process. This should not + * exceed the scratch buffer size and should atleast be 8 bytes */ +#define PARSER_BLOCK_SIZE 128 + +/* Calculate the maximum size needed for the scratch buffer */ +#define HTTPD_SCRATCH_BUF MAX(HTTPD_MAX_REQ_HDR_LEN, HTTPD_MAX_URI_LEN) + +/* Formats a log string to prepend context function name */ +#define LOG_FMT(x) "%s: " x, __func__ + +/** + * @brief Thread related data for internal use + */ +struct thread_data { + othread_t handle; /*!< Handle to thread/task */ + enum { + THREAD_IDLE = 0, + THREAD_RUNNING, + THREAD_STOPPING, + THREAD_STOPPED, + } status; /*!< State of the thread */ +}; + +/** + * @brief Error codes sent by server in case of errors + * encountered during processing of an HTTP request + */ +typedef enum { + /* For any unexpected errors during parsing, like unexpected + * state transitions, or unhandled errors. + */ + HTTPD_500_SERVER_ERROR = 0, + + /* For methods not supported by http_parser. Presently + * http_parser halts parsing when such methods are + * encountered and so the server responds with 400 Bad + * Request error instead. + */ + HTTPD_501_METHOD_NOT_IMPLEMENTED, + + /* When HTTP version is not 1.1 */ + HTTPD_505_VERSION_NOT_SUPPORTED, + + /* Returned when http_parser halts parsing due to incorrect + * syntax of request, unsupported method in request URI or + * due to chunked encoding option present in headers + */ + HTTPD_400_BAD_REQUEST, + + /* When requested URI is not found */ + HTTPD_404_NOT_FOUND, + + /* When URI found, but method has no handler registered */ + HTTPD_405_METHOD_NOT_ALLOWED, + + /* Intended for recv timeout. Presently it's being sent + * for other recv errors as well. Client should expect the + * server to immediatly close the connection after + * responding with this. + */ + HTTPD_408_REQ_TIMEOUT, + + /* Intended for responding to chunked encoding, which is + * not supported currently. Though unhandled http_parser + * callback for chunked request returns "400 Bad Request" + */ + HTTPD_411_LENGTH_REQUIRED, + + /* URI length greater than HTTPD_MAX_URI_LEN */ + HTTPD_414_URI_TOO_LONG, + + /* Headers section larger thn HTTPD_MAX_REQ_HDR_LEN */ + HTTPD_431_REQ_HDR_FIELDS_TOO_LARGE, + + /* There is no particular HTTP error code for not supporting + * upgrade. For this respond with 200 OK. Client expects status + * code 101 if upgrade were supported, so 200 should be fine. + */ + HTTPD_XXX_UPGRADE_NOT_SUPPORTED +} httpd_err_resp_t; + +/** + * @brief A database of all the open sockets in the system. + */ +struct sock_db { + int fd; /*!< The file descriptor for this socket */ + void *ctx; /*!< A custom context for this socket */ + void *transport_ctx; /*!< A custom 'transport' context for this socket, to be used by send/recv/pending */ + httpd_handle_t handle; /*!< Server handle */ + httpd_free_ctx_fn_t free_ctx; /*!< Function for freeing the context */ + httpd_free_ctx_fn_t free_transport_ctx; /*!< Function for freeing the 'transport' context */ + httpd_send_func_t send_fn; /*!< Send function for this socket */ + httpd_recv_func_t recv_fn; /*!< Receive function for this socket */ + httpd_pending_func_t pending_fn; /*!< Pending function for this socket */ + int64_t timestamp; /*!< Timestamp indicating when the socket was last used */ + char pending_data[PARSER_BLOCK_SIZE]; /*!< Buffer for pending data to be received */ + size_t pending_len; /*!< Length of pending data to be received */ +}; + +/** + * @brief Auxilary data structure for use during reception and processing + * of requests and temporarily keeping responses + */ +struct httpd_req_aux { + struct sock_db *sd; /*!< Pointer to socket database */ + char scratch[HTTPD_SCRATCH_BUF + 1]; /*!< Temporary buffer for our operations (1 byte extra for null termination) */ + size_t remaining_len; /*!< Amount of data remaining to be fetched */ + char *status; /*!< HTTP response's status code */ + char *content_type; /*!< HTTP response's content type */ + bool first_chunk_sent; /*!< Used to indicate if first chunk sent */ + unsigned req_hdrs_count; /*!< Count of total headers in request packet */ + unsigned resp_hdrs_count; /*!< Count of additional headers in response packet */ + struct resp_hdr { + const char *field; + const char *value; + } *resp_hdrs; /*!< Additional headers in response packet */ + struct http_parser_url url_parse_res; /*!< URL parsing result, used for retrieving URL elements */ +}; + +/** + * @brief Server data for each instance. This is exposed publicaly as + * httpd_handle_t but internal structure/members are kept private. + */ +struct httpd_data { + httpd_config_t config; /*!< HTTPD server configuration */ + int listen_fd; /*!< Server listener FD */ + int ctrl_fd; /*!< Ctrl message receiver FD */ + int msg_fd; /*!< Ctrl message sender FD */ + struct thread_data hd_td; /*!< Information for the HTTPd thread */ + struct sock_db *hd_sd; /*!< The socket database */ + httpd_uri_t **hd_calls; /*!< Registered URI handlers */ + struct httpd_req hd_req; /*!< The current HTTPD request */ + struct httpd_req_aux hd_req_aux; /*!< Additional data about the HTTPD request kept unexposed */ +}; + +/******************* Group : Session Management ********************/ +/** @name Session Management + * Functions related to HTTP session management + * @{ + */ + +/** + * @brief Retrieve a session by its descriptor + * + * @param[in] hd Server instance data + * @param[in] sockfd Socket FD + * @return pointer into the socket DB, or NULL if not found + */ +struct sock_db *httpd_sess_get(struct httpd_data *hd, int sockfd); + +/** + * @brief Delete sessions whose FDs have became invalid. + * This is a recovery strategy e.g. after select() fails. + * + * @param[in] hd Server instance data + */ +void httpd_sess_delete_invalid(struct httpd_data *hd); + +/** + * @brief Initializes an http session by resetting the sockets database. + * + * @param[in] hd Server instance data + */ +void httpd_sess_init(struct httpd_data *hd); + +/** + * @brief Starts a new session for client requesting connection and adds + * it's descriptor to the socket database. + * + * @param[in] hd Server instance data + * @param[in] newfd Descriptor of the new client to be added to the session. + * + * @return + * - ESP_OK : on successfully queueing the work + * - ESP_FAIL : in case of control socket error while sending + */ +esp_err_t httpd_sess_new(struct httpd_data *hd, int newfd); + +/** + * @brief Processes incoming HTTP requests + * + * @param[in] hd Server instance data + * @param[in] clifd Descriptor of the client from which data is to be received + * + * @return + * - ESP_OK : on successfully receiving, parsing and responding to a request + * - ESP_FAIL : in case of failure in any of the stages of processing + */ +esp_err_t httpd_sess_process(struct httpd_data *hd, int clifd); + +/** + * @brief Remove client descriptor from the session / socket database + * and close the connection for this client. + * + * @note The returned descriptor should be used by httpd_sess_iterate() + * to continue the iteration correctly. This ensurs that the + * iteration is not restarted abruptly which may cause reading from + * a socket which has been already processed and thus blocking + * the server loop until data appears on that socket. + * + * @param[in] hd Server instance data + * @param[in] clifd Descriptor of the client to be removed from the session. + * + * @return + * - +VE : Client descriptor preceding the one being deleted + * - -1 : No descriptor preceding the one being deleted + */ +int httpd_sess_delete(struct httpd_data *hd, int clifd); + +/** + * @brief Free session context + * + * @param[in] ctx Pointer to session context + * @param[in] free_fn Free function to call on session context + */ +void httpd_sess_free_ctx(void *ctx, httpd_free_ctx_fn_t free_fn); + +/** + * @brief Add descriptors present in the socket database to an fd_set and + * update the value of maxfd which are needed by the select function + * for looking through all available sockets for incoming data. + * + * @param[in] hd Server instance data + * @param[out] fdset File descriptor set to be updated. + * @param[out] maxfd Maximum value among all file descriptors. + */ +void httpd_sess_set_descriptors(struct httpd_data *hd, fd_set *fdset, int *maxfd); + +/** + * @brief Iterates through the list of client fds in the session /socket database. + * Passing the value of a client fd returns the fd for the next client + * in the database. In order to iterate from the beginning pass -1 as fd. + * + * @param[in] hd Server instance data + * @param[in] fd Last accessed client descriptor. + * -1 to reset iterator to start of database. + * + * @return + * - +VE : Client descriptor next in the database + * - -1 : End of iteration + */ +int httpd_sess_iterate(struct httpd_data *hd, int fd); + +/** + * @brief Checks if session can accept another connection from new client. + * If sockets database is full then this returns false. + * + * @param[in] hd Server instance data + * + * @return True if session can accept new clients + */ +bool httpd_is_sess_available(struct httpd_data *hd); + +/** + * @brief Checks if session has any pending data/packets + * for processing + * + * This is needed as httpd_unrecv may unreceive next + * packet in the stream. If only partial packet was + * received then select() would mark the fd for processing + * as remaining part of the packet would still be in socket + * recv queue. But if a complete packet got unreceived + * then it would not be processed until furtur data is + * received on the socket. This is when this function + * comes in use, as it checks the socket's pending data + * buffer. + * + * @param[in] hd Server instance data + * @param[in] fd Client descriptor + * + * @return True if there is any pending data + */ +bool httpd_sess_pending(struct httpd_data *hd, int fd); + +/** + * @brief Removes the least recently used client from the session + * + * This may be useful if new clients are requesting for connection but + * max number of connections is reached, in which case the client which + * is inactive for the longest will be removed from the session. + * + * @param[in] hd Server instance data + * + * @return + * - ESP_OK : if session closure initiated successfully + * - ESP_FAIL : if failed + */ +esp_err_t httpd_sess_close_lru(struct httpd_data *hd); + +/** End of Group : Session Management + * @} + */ + +/****************** Group : URI Handling ********************/ +/** @name URI Handling + * Methods for accessing URI handlers + * @{ + */ + +/** + * @brief For an HTTP request, searches through all the registered URI handlers + * and invokes the appropriate one if found + * + * @param[in] hd Server instance data for which handler needs to be invoked + * + * @return + * - ESP_OK : if handler found and executed successfully + * - ESP_FAIL : otherwise + */ +esp_err_t httpd_uri(struct httpd_data *hd); + +/** + * @brief Deregister all URI handlers + * + * @param[in] hd Server instance data + */ +void httpd_unregister_all_uri_handlers(struct httpd_data *hd); + +/** + * @brief Validates the request to prevent users from calling APIs, that are to + * be called only inside a URI handler, outside the handler context + * + * @param[in] req Pointer to HTTP request that neds to be validated + * + * @return + * - true : if valid request + * - false : otherwise + */ +bool httpd_validate_req_ptr(httpd_req_t *r); + +/* httpd_validate_req_ptr() adds some overhead to frequently used APIs, + * and is useful mostly for debugging, so it's preferable to disable + * the check by defaut and enable it only if necessary */ +#ifdef CONFIG_HTTPD_VALIDATE_REQ +#define httpd_valid_req(r) httpd_validate_req_ptr(r) +#else +#define httpd_valid_req(r) true +#endif + +/** End of Group : URI Handling + * @} + */ + +/****************** Group : Processing ********************/ +/** @name Processing + * Methods for processing HTTP requests + * @{ + */ + +/** + * @brief Initiates the processing of HTTP request + * + * Receives incoming TCP packet on a socket, then parses the packet as + * HTTP request and fills httpd_req_t data structure with the extracted + * URI, headers are ready to be fetched from scratch buffer and calling + * http_recv() after this reads the body of the request. + * + * @param[in] hd Server instance data + * @param[in] sd Pointer to socket which is needed for receiving TCP packets. + * + * @return + * - ESP_OK : if request packet is valid + * - ESP_FAIL : otherwise + */ +esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd); + +/** + * @brief For an HTTP request, resets the resources allocated for it and + * purges any data left to be received + * + * @param[in] hd Server instance data + * + * @return + * - ESP_OK : if request packet deleted and resources cleaned. + * - ESP_FAIL : otherwise. + */ +esp_err_t httpd_req_delete(struct httpd_data *hd); + +/** End of Group : Parsing + * @} + */ + +/****************** Group : Send/Receive ********************/ +/** @name Send and Receive + * Methods for transmitting and receiving HTTP requests and responses + * @{ + */ + +/** + * @brief For sending out error code in response to HTTP request. + * + * @param[in] req Pointer to the HTTP request for which the resonse needs to be sent + * @param[in] error Error type to send + * + * @return + * - ESP_OK : if successful + * - ESP_FAIL : if failed + */ +esp_err_t httpd_resp_send_err(httpd_req_t *req, httpd_err_resp_t error); + +/** + * @brief For sending out data in response to an HTTP request. + * + * @param[in] req Pointer to the HTTP request for which the resonse needs to be sent + * @param[in] buf Pointer to the buffer from where the body of the response is taken + * @param[in] buf_len Length of the buffer + * + * @return + * - Length of data : if successful + * - ESP_FAIL : if failed + */ +int httpd_send(httpd_req_t *req, const char *buf, size_t buf_len); + +/** + * @brief For receiving HTTP request data + * + * @note The exposed API httpd_recv() is simply this function with last parameter + * set as false. This function is used internally during reception and + * processing of a new request. The option to halt after receiving pending + * data prevents the server from requesting more data than is needed for + * completing a packet in case when all the remaining part of the packet is + * in the pending buffer. + * + * @param[in] req Pointer to new HTTP request which only has the socket descriptor + * @param[out] buf Pointer to the buffer which will be filled with the received data + * @param[in] buf_len Length of the buffer + * @param[in] halt_after_pending When set true, halts immediatly after receiving from + * pending buffer + * + * @return + * - Length of data : if successful + * - ESP_FAIL : if failed + */ +int httpd_recv_with_opt(httpd_req_t *r, char *buf, size_t buf_len, bool halt_after_pending); + +/** + * @brief For un-receiving HTTP request data + * + * This function copies data into internal buffer pending_data so that + * when httpd_recv is called, it first fetches this pending data and + * then only starts receiving from the socket + * + * @note If data is too large for the internal buffer then only + * part of the data is unreceived, reflected in the returned + * length. Make sure that such truncation is checked for and + * handled properly. + * + * @param[in] req Pointer to new HTTP request which only has the socket descriptor + * @param[in] buf Pointer to the buffer from where data needs to be un-received + * @param[in] buf_len Length of the buffer + * + * @return Length of data copied into pending buffer + */ +size_t httpd_unrecv(struct httpd_req *r, const char *buf, size_t buf_len); + +/** + * @brief This is the low level default send function of the HTTPD. This should + * NEVER be called directly. The semantics of this is exactly similar to + * send() of the BSD socket API. + * + * @param[in] hd Server instance data + * @param[in] sockfd Socket descriptor for sending data + * @param[in] buf Pointer to the buffer from where the body of the response is taken + * @param[in] buf_len Length of the buffer + * @param[in] flags Flags for mode selection + * + * @return + * - Length of data : if successful + * - -1 : if failed (appropriate errno is set) + */ +int httpd_default_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags); + +/** + * @brief This is the low level default recv function of the HTTPD. This should + * NEVER be called directly. The semantics of this is exactly similar to + * recv() of the BSD socket API. + * + * @param[in] hd Server instance data + * @param[in] sockfd Socket descriptor for sending data + * @param[out] buf Pointer to the buffer which will be filled with the received data + * @param[in] buf_len Length of the buffer + * @param[in] flags Flags for mode selection + * + * @return + * - Length of data : if successful + * - -1 : if failed (appropriate errno is set) + */ +int httpd_default_recv(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, int flags); + +/** End of Group : Send and Receive + * @} + */ + +#ifdef __cplusplus +} +#endif + +#endif /* ! _HTTPD_PRIV_H_ */ diff --git a/components/esp_http_server/src/httpd_main.c b/components/esp_http_server/src/httpd_main.c new file mode 100644 index 00000000..dc4cbd86 --- /dev/null +++ b/components/esp_http_server/src/httpd_main.c @@ -0,0 +1,403 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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 <string.h> +#include <sys/socket.h> +#include <sys/param.h> +#include <errno.h> +#include <esp_log.h> +#include <esp_err.h> +#include <assert.h> + +#include <esp_http_server.h> +#include "esp_httpd_priv.h" +#include "ctrl_sock.h" + +static const char *TAG = "httpd"; + +static esp_err_t httpd_accept_conn(struct httpd_data *hd, int listen_fd) +{ + /* If no space is available for new session, close the least recently used one */ + if (hd->config.lru_purge_enable == true) { + if (!httpd_is_sess_available(hd)) { + /* Queue asynchronous closure of the least recently used session */ + return httpd_sess_close_lru(hd); + /* Returning from this allowes the main server thread to process + * the queued asynchronous control message for closing LRU session. + * Since connection request hasn't been addressed yet using accept() + * therefore httpd_accept_conn() will be called again, but this time + * with space available for one session + */ + } + } + + struct sockaddr_in addr_from; + socklen_t addr_from_len = sizeof(addr_from); + int new_fd = accept(listen_fd, (struct sockaddr *)&addr_from, &addr_from_len); + if (new_fd < 0) { + ESP_LOGW(TAG, LOG_FMT("error in accept (%d)"), errno); + return ESP_FAIL; + } + ESP_LOGD(TAG, LOG_FMT("newfd = %d"), new_fd); + + struct timeval tv; + /* Set recv timeout of this fd as per config */ + tv.tv_sec = hd->config.recv_wait_timeout; + tv.tv_usec = 0; + setsockopt(new_fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); + + /* Set send timeout of this fd as per config */ + tv.tv_sec = hd->config.send_wait_timeout; + tv.tv_usec = 0; + setsockopt(new_fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv)); + + if (ESP_OK != httpd_sess_new(hd, new_fd)) { + ESP_LOGW(TAG, LOG_FMT("session creation failed")); + close(new_fd); + return ESP_FAIL; + } + ESP_LOGD(TAG, LOG_FMT("complete")); + return ESP_OK; +} + +struct httpd_ctrl_data { + enum httpd_ctrl_msg { + HTTPD_CTRL_SHUTDOWN, + HTTPD_CTRL_WORK, + } hc_msg; + httpd_work_fn_t hc_work; + void *hc_work_arg; +}; + +esp_err_t httpd_queue_work(httpd_handle_t handle, httpd_work_fn_t work, void *arg) +{ + if (handle == NULL || work == NULL) { + return ESP_ERR_INVALID_ARG; + } + + struct httpd_data *hd = (struct httpd_data *) handle; + struct httpd_ctrl_data msg = { + .hc_msg = HTTPD_CTRL_WORK, + .hc_work = work, + .hc_work_arg = arg, + }; + + int ret = cs_send_to_ctrl_sock(hd->msg_fd, hd->config.ctrl_port, &msg, sizeof(msg)); + if (ret < 0) { + ESP_LOGW(TAG, LOG_FMT("failed to queue work")); + return ESP_FAIL; + } + + return ESP_OK; +} + +void *httpd_get_global_user_ctx(httpd_handle_t handle) +{ + return ((struct httpd_data *)handle)->config.global_user_ctx; +} + +void *httpd_get_global_transport_ctx(httpd_handle_t handle) +{ + return ((struct httpd_data *)handle)->config.global_transport_ctx; +} + +static void httpd_close_all_sessions(struct httpd_data *hd) +{ + int fd = -1; + while ((fd = httpd_sess_iterate(hd, fd)) != -1) { + ESP_LOGD(TAG, LOG_FMT("cleaning up socket %d"), fd); + httpd_sess_delete(hd, fd); + close(fd); + } +} + +static void httpd_process_ctrl_msg(struct httpd_data *hd) +{ + struct httpd_ctrl_data msg; + int ret = recv(hd->ctrl_fd, &msg, sizeof(msg), 0); + if (ret <= 0) { + ESP_LOGW(TAG, LOG_FMT("error in recv (%d)"), errno); + return; + } + if (ret != sizeof(msg)) { + ESP_LOGW(TAG, LOG_FMT("incomplete msg")); + return; + } + + switch (msg.hc_msg) { + case HTTPD_CTRL_WORK: + if (msg.hc_work) { + ESP_LOGD(TAG, LOG_FMT("work")); + (*msg.hc_work)(msg.hc_work_arg); + } + break; + case HTTPD_CTRL_SHUTDOWN: + ESP_LOGD(TAG, LOG_FMT("shutdown")); + hd->hd_td.status = THREAD_STOPPING; + break; + default: + break; + } +} + +/* Manage in-coming connection or data requests */ +static esp_err_t httpd_server(struct httpd_data *hd) +{ + fd_set read_set; + FD_ZERO(&read_set); + FD_SET(hd->listen_fd, &read_set); + FD_SET(hd->ctrl_fd, &read_set); + + int tmp_max_fd; + httpd_sess_set_descriptors(hd, &read_set, &tmp_max_fd); + int maxfd = MAX(hd->listen_fd, tmp_max_fd); + tmp_max_fd = maxfd; + maxfd = MAX(hd->ctrl_fd, tmp_max_fd); + + ESP_LOGD(TAG, LOG_FMT("doing select maxfd+1 = %d"), maxfd + 1); + int active_cnt = select(maxfd + 1, &read_set, NULL, NULL, NULL); + if (active_cnt < 0) { + ESP_LOGE(TAG, LOG_FMT("error in select (%d)"), errno); + httpd_sess_delete_invalid(hd); + return ESP_OK; + } + + /* Case0: Do we have a control message? */ + if (FD_ISSET(hd->ctrl_fd, &read_set)) { + ESP_LOGD(TAG, LOG_FMT("processing ctrl message")); + httpd_process_ctrl_msg(hd); + if (hd->hd_td.status == THREAD_STOPPING) { + ESP_LOGD(TAG, LOG_FMT("stopping thread")); + return ESP_FAIL; + } + } + + /* Case1: Do we have any activity on the current data + * sessions? */ + int fd = -1; + while ((fd = httpd_sess_iterate(hd, fd)) != -1) { + if (FD_ISSET(fd, &read_set) || (httpd_sess_pending(hd, fd))) { + ESP_LOGD(TAG, LOG_FMT("processing socket %d"), fd); + if (httpd_sess_process(hd, fd) != ESP_OK) { + ESP_LOGD(TAG, LOG_FMT("closing socket %d"), fd); + close(fd); + /* Delete session and update fd to that + * preceding the one being deleted */ + fd = httpd_sess_delete(hd, fd); + } + } + } + + /* Case2: Do we have any incoming connection requests to + * process? */ + if (FD_ISSET(hd->listen_fd, &read_set)) { + ESP_LOGD(TAG, LOG_FMT("processing listen socket %d"), hd->listen_fd); + if (httpd_accept_conn(hd, hd->listen_fd) != ESP_OK) { + ESP_LOGW(TAG, LOG_FMT("error accepting new connection")); + } + } + return ESP_OK; +} + +/* The main HTTPD thread */ +static void httpd_thread(void *arg) +{ + int ret; + struct httpd_data *hd = (struct httpd_data *) arg; + hd->hd_td.status = THREAD_RUNNING; + + ESP_LOGD(TAG, LOG_FMT("web server started")); + while (1) { + ret = httpd_server(hd); + if (ret != ESP_OK) { + break; + } + } + + ESP_LOGD(TAG, LOG_FMT("web server exiting")); + close(hd->msg_fd); + cs_free_ctrl_sock(hd->ctrl_fd); + httpd_close_all_sessions(hd); + close(hd->listen_fd); + hd->hd_td.status = THREAD_STOPPED; + httpd_os_thread_delete(); +} + +static esp_err_t httpd_server_init(struct httpd_data *hd) +{ + int fd = socket(PF_INET6, SOCK_STREAM, 0); + if (fd < 0) { + ESP_LOGE(TAG, LOG_FMT("error in socket (%d)"), errno); + return ESP_FAIL; + } + + struct in6_addr inaddr_any = IN6ADDR_ANY_INIT; + struct sockaddr_in6 serv_addr = { + .sin6_family = PF_INET6, + .sin6_addr = inaddr_any, + .sin6_port = htons(hd->config.server_port) + }; + + int ret = bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); + if (ret < 0) { + ESP_LOGE(TAG, LOG_FMT("error in bind (%d)"), errno); + close(fd); + return ESP_FAIL; + } + + ret = listen(fd, hd->config.backlog_conn); + if (ret < 0) { + ESP_LOGE(TAG, LOG_FMT("error in listen (%d)"), errno); + close(fd); + return ESP_FAIL; + } + + int ctrl_fd = cs_create_ctrl_sock(hd->config.ctrl_port); + if (ctrl_fd < 0) { + ESP_LOGE(TAG, LOG_FMT("error in creating ctrl socket (%d)"), errno); + close(fd); + return ESP_FAIL; + } + + int msg_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (msg_fd < 0) { + ESP_LOGE(TAG, LOG_FMT("error in creating msg socket (%d)"), errno); + close(fd); + close(ctrl_fd); + return ESP_FAIL; + } + + hd->listen_fd = fd; + hd->ctrl_fd = ctrl_fd; + hd->msg_fd = msg_fd; + return ESP_OK; +} + +static struct httpd_data *httpd_create(const httpd_config_t *config) +{ + /* Allocate memory for httpd instance data */ + struct httpd_data *hd = calloc(1, sizeof(struct httpd_data)); + if (hd != NULL) { + hd->hd_calls = calloc(config->max_uri_handlers, sizeof(httpd_uri_t *)); + if (hd->hd_calls == NULL) { + free(hd); + return NULL; + } + hd->hd_sd = calloc(config->max_open_sockets, sizeof(struct sock_db)); + if (hd->hd_sd == NULL) { + free(hd->hd_calls); + free(hd); + return NULL; + } + struct httpd_req_aux *ra = &hd->hd_req_aux; + ra->resp_hdrs = calloc(config->max_resp_headers, sizeof(struct resp_hdr)); + if (ra->resp_hdrs == NULL) { + free(hd->hd_sd); + free(hd->hd_calls); + free(hd); + return NULL; + } + /* Save the configuration for this instance */ + hd->config = *config; + } else { + ESP_LOGE(TAG, "mem alloc failed"); + } + return hd; +} + +static void httpd_delete(struct httpd_data *hd) +{ + struct httpd_req_aux *ra = &hd->hd_req_aux; + /* Free memory of httpd instance data */ + free(ra->resp_hdrs); + free(hd->hd_sd); + + /* Free registered URI handlers */ + httpd_unregister_all_uri_handlers(hd); + free(hd->hd_calls); + free(hd); +} + +esp_err_t httpd_start(httpd_handle_t *handle, const httpd_config_t *config) +{ + if (handle == NULL || config == NULL) { + return ESP_ERR_INVALID_ARG; + } + + struct httpd_data *hd = httpd_create(config); + if (hd == NULL) { + /* Failed to allocate memory */ + return ESP_ERR_HTTPD_ALLOC_MEM; + } + + if (httpd_server_init(hd) != ESP_OK) { + httpd_delete(hd); + return ESP_FAIL; + } + + httpd_sess_init(hd); + if (httpd_os_thread_create(&hd->hd_td.handle, "httpd", + hd->config.stack_size, + hd->config.task_priority, + httpd_thread, hd) != ESP_OK) { + /* Failed to launch task */ + httpd_delete(hd); + return ESP_ERR_HTTPD_TASK; + } + + *handle = (httpd_handle_t *)hd; + return ESP_OK; +} + +esp_err_t httpd_stop(httpd_handle_t handle) +{ + struct httpd_data *hd = (struct httpd_data *) handle; + if (hd == NULL) { + return ESP_ERR_INVALID_ARG; + } + + struct httpd_ctrl_data msg; + memset(&msg, 0, sizeof(msg)); + msg.hc_msg = HTTPD_CTRL_SHUTDOWN; + cs_send_to_ctrl_sock(hd->msg_fd, hd->config.ctrl_port, &msg, sizeof(msg)); + + ESP_LOGD(TAG, LOG_FMT("sent control msg to stop server")); + while (hd->hd_td.status != THREAD_STOPPED) { + httpd_os_thread_sleep(100); + } + + /* Release global user context, if not NULL */ + if (hd->config.global_user_ctx) { + if (hd->config.global_user_ctx_free_fn) { + hd->config.global_user_ctx_free_fn(hd->config.global_user_ctx); + } else { + free(hd->config.global_user_ctx); + } + hd->config.global_user_ctx = NULL; + } + + /* Release global transport context, if not NULL */ + if (hd->config.global_transport_ctx) { + if (hd->config.global_transport_ctx_free_fn) { + hd->config.global_transport_ctx_free_fn(hd->config.global_transport_ctx); + } else { + free(hd->config.global_transport_ctx); + } + hd->config.global_transport_ctx = NULL; + } + + ESP_LOGD(TAG, LOG_FMT("server stopped")); + httpd_delete(hd); + return ESP_OK; +} diff --git a/components/esp_http_server/src/httpd_parse.c b/components/esp_http_server/src/httpd_parse.c new file mode 100644 index 00000000..51843182 --- /dev/null +++ b/components/esp_http_server/src/httpd_parse.c @@ -0,0 +1,856 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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 <stdlib.h> +#include <sys/param.h> +#include <esp_log.h> +#include <esp_err.h> +#include <http_parser.h> + +#include <esp_http_server.h> +#include "esp_httpd_priv.h" +#include "osal.h" + +static const char *TAG = "httpd_parse"; + +typedef struct { + /* Parser settings for http_parser_execute() */ + http_parser_settings settings; + + /* Request being parsed */ + struct httpd_req *req; + + /* Status of the parser describes the part of the + * HTTP request packet being processed at any moment. + */ + enum { + PARSING_IDLE = 0, + PARSING_URL, + PARSING_HDR_FIELD, + PARSING_HDR_VALUE, + PARSING_BODY, + PARSING_COMPLETE, + PARSING_FAILED + } status; + + /* Response error code in case of PARSING_FAILED */ + httpd_err_resp_t error; + + /* For storing last callback parameters */ + struct { + const char *at; + size_t length; + } last; + + /* State variables */ + bool paused; /*!< Parser is paused */ + size_t pre_parsed; /*!< Length of data to be skipped while parsing */ + size_t raw_datalen; /*!< Full length of the raw data in scratch buffer */ +} parser_data_t; + +static esp_err_t verify_url (http_parser *parser) +{ + parser_data_t *parser_data = (parser_data_t *) parser->data; + struct httpd_req *r = parser_data->req; + struct httpd_req_aux *ra = r->aux; + struct http_parser_url *res = &ra->url_parse_res; + + /* Get previous values of the parser callback arguments */ + const char *at = parser_data->last.at; + size_t length = parser_data->last.length; + + if ((r->method = parser->method) < 0) { + ESP_LOGW(TAG, LOG_FMT("HTTP Operation not supported")); + parser_data->error = HTTPD_501_METHOD_NOT_IMPLEMENTED; + return ESP_FAIL; + } + + if (sizeof(r->uri) < (length + 1)) { + ESP_LOGW(TAG, LOG_FMT("URI length (%d) greater than supported (%d)"), + length, sizeof(r->uri)); + parser_data->error = HTTPD_414_URI_TOO_LONG; + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + /* Keep URI with terminating null character. Note URI string pointed + * by 'at' is not NULL terminated, therefore use length provided by + * parser while copying the URI to buffer */ + strlcpy((char *)r->uri, at, (length + 1)); + ESP_LOGD(TAG, LOG_FMT("received URI = %s"), r->uri); + + /* Make sure version is HTTP/1.1 */ + if ((parser->http_major != 1) && (parser->http_minor != 1)) { + ESP_LOGW(TAG, LOG_FMT("unsupported HTTP version = %d.%d"), + parser->http_major, parser->http_minor); + parser_data->error = HTTPD_505_VERSION_NOT_SUPPORTED; + return ESP_FAIL; + } + + /* Parse URL and keep result for later */ + http_parser_url_init(res); + if (http_parser_parse_url(r->uri, strlen(r->uri), + r->method == HTTP_CONNECT, res)) { + ESP_LOGW(TAG, LOG_FMT("http_parser_parse_url failed with errno = %d"), + parser->http_errno); + parser_data->error = HTTPD_400_BAD_REQUEST; + return ESP_FAIL; + } + return ESP_OK; +} + +/* http_parser callback on finding url in HTTP request + * Will be invoked ATLEAST once every packet + */ +static esp_err_t cb_url(http_parser *parser, + const char *at, size_t length) +{ + parser_data_t *parser_data = (parser_data_t *) parser->data; + + if (parser_data->status == PARSING_IDLE) { + ESP_LOGD(TAG, LOG_FMT("message begin")); + + /* Store current values of the parser callback arguments */ + parser_data->last.at = at; + parser_data->last.length = 0; + parser_data->status = PARSING_URL; + } else if (parser_data->status != PARSING_URL) { + ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + ESP_LOGD(TAG, LOG_FMT("processing url = %.*s"), length, at); + + /* Update length of URL string */ + if ((parser_data->last.length += length) > HTTPD_MAX_URI_LEN) { + ESP_LOGW(TAG, LOG_FMT("URI length (%d) greater than supported (%d)"), + parser_data->last.length, HTTPD_MAX_URI_LEN); + parser_data->error = HTTPD_414_URI_TOO_LONG; + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + return ESP_OK; +} + +static esp_err_t pause_parsing(http_parser *parser, const char* at) +{ + parser_data_t *parser_data = (parser_data_t *) parser->data; + struct httpd_req *r = parser_data->req; + struct httpd_req_aux *ra = r->aux; + + parser_data->pre_parsed = parser_data->raw_datalen + - (at - ra->scratch); + + if (parser_data->pre_parsed != httpd_unrecv(r, at, parser_data->pre_parsed)) { + ESP_LOGE(TAG, LOG_FMT("data too large for un-recv = %d"), + parser_data->pre_parsed); + return ESP_FAIL; + } + + http_parser_pause(parser, 1); + parser_data->paused = true; + ESP_LOGD(TAG, LOG_FMT("paused")); + return ESP_OK; +} + +static size_t continue_parsing(http_parser *parser, size_t length) +{ + parser_data_t *data = (parser_data_t *) parser->data; + + /* Part of the blk may have been parsed before + * so we must skip that */ + length = MIN(length, data->pre_parsed); + data->pre_parsed -= length; + ESP_LOGD(TAG, LOG_FMT("skip pre-parsed data of size = %d"), length); + + http_parser_pause(parser, 0); + data->paused = false; + ESP_LOGD(TAG, LOG_FMT("un-paused")); + return length; +} + +/* http_parser callback on header field in HTTP request + * May be invoked ATLEAST once every header field + */ +static esp_err_t cb_header_field(http_parser *parser, const char *at, size_t length) +{ + parser_data_t *parser_data = (parser_data_t *) parser->data; + struct httpd_req *r = parser_data->req; + struct httpd_req_aux *ra = r->aux; + + /* Check previous status */ + if (parser_data->status == PARSING_URL) { + if (verify_url(parser) != ESP_OK) { + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + ESP_LOGD(TAG, LOG_FMT("headers begin")); + /* Last at is set to start of scratch where headers + * will be received next */ + parser_data->last.at = ra->scratch; + parser_data->last.length = 0; + parser_data->status = PARSING_HDR_FIELD; + + /* Stop parsing for now and give control to process */ + if (pause_parsing(parser, at) != ESP_OK) { + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + } else if (parser_data->status == PARSING_HDR_VALUE) { + /* NULL terminate last header (key: value) pair */ + size_t offset = parser_data->last.at - ra->scratch; + ra->scratch[offset + parser_data->last.length] = '\0'; + + /* Store current values of the parser callback arguments */ + parser_data->last.at = at; + parser_data->last.length = 0; + parser_data->status = PARSING_HDR_FIELD; + } else if (parser_data->status != PARSING_HDR_FIELD) { + ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + ESP_LOGD(TAG, LOG_FMT("processing field = %.*s"), length, at); + + /* Update length of header string */ + parser_data->last.length += length; + return ESP_OK; +} + +/* http_parser callback on header value in HTTP request. + * May be invoked ATLEAST once every header value + */ +static esp_err_t cb_header_value(http_parser *parser, const char *at, size_t length) +{ + parser_data_t *parser_data = (parser_data_t *) parser->data; + struct httpd_req *r = parser_data->req; + struct httpd_req_aux *ra = r->aux; + + /* Check previous status */ + if (parser_data->status == PARSING_HDR_FIELD) { + /* Store current values of the parser callback arguments */ + parser_data->last.at = at; + parser_data->last.length = 0; + parser_data->status = PARSING_HDR_VALUE; + /* Increment header count */ + ra->req_hdrs_count++; + } else if (parser_data->status != PARSING_HDR_VALUE) { + ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + ESP_LOGD(TAG, LOG_FMT("processing value = %.*s"), length, at); + + /* Update length of header string */ + parser_data->last.length += length; + return ESP_OK; +} + +/* http_parser callback on completing headers in HTTP request. + * Will be invoked ONLY once every packet + */ +static esp_err_t cb_headers_complete(http_parser *parser) +{ + parser_data_t *parser_data = (parser_data_t *) parser->data; + struct httpd_req *r = parser_data->req; + struct httpd_req_aux *ra = r->aux; + + /* Check previous status */ + if (parser_data->status == PARSING_URL) { + ESP_LOGD(TAG, LOG_FMT("no headers")); + if (verify_url(parser) != ESP_OK) { + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + } else if (parser_data->status == PARSING_HDR_VALUE) { + /* NULL terminate last header (key: value) pair */ + size_t offset = parser_data->last.at - ra->scratch; + ra->scratch[offset + parser_data->last.length] = '\0'; + + /* Reach end of last header */ + parser_data->last.at += parser_data->last.length; + } else { + ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + /* In absence of body/chunked encoding, http_parser sets content_len to -1 */ + r->content_len = ((int)parser->content_length != -1 ? + parser->content_length : 0); + + ESP_LOGD(TAG, LOG_FMT("bytes read = %d"), parser->nread); + ESP_LOGD(TAG, LOG_FMT("content length = %zu"), r->content_len); + + if (parser->upgrade) { + ESP_LOGW(TAG, LOG_FMT("upgrade from HTTP not supported")); + parser_data->error = HTTPD_XXX_UPGRADE_NOT_SUPPORTED; + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + parser_data->status = PARSING_BODY; + ra->remaining_len = r->content_len; + return ESP_OK; +} + +/* Last http_parser callback if body present in HTTP request. + * Will be invoked ONLY once every packet + */ +static esp_err_t cb_on_body(http_parser *parser, const char *at, size_t length) +{ + parser_data_t *parser_data = (parser_data_t *) parser->data; + + /* Check previous status */ + if (parser_data->status != PARSING_BODY) { + ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + /* Pause parsing so that if part of another packet + * is in queue then it doesn't get parsed, which + * may reset the parser state and cause current + * request packet to be lost */ + if (pause_parsing(parser, at) != ESP_OK) { + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + parser_data->last.at = 0; + parser_data->last.length = 0; + parser_data->status = PARSING_COMPLETE; + ESP_LOGD(TAG, LOG_FMT("body begins")); + return ESP_OK; +} + +/* Last http_parser callback if body absent in HTTP request. + * Will be invoked ONLY once every packet + */ +static esp_err_t cb_no_body(http_parser *parser) +{ + parser_data_t *parser_data = (parser_data_t *) parser->data; + const char* at = parser_data->last.at; + + /* Check previous status */ + if (parser_data->status == PARSING_URL) { + ESP_LOGD(TAG, LOG_FMT("no headers")); + if (verify_url(parser) != ESP_OK) { + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + } else if (parser_data->status != PARSING_BODY) { + ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + /* Get end of packet */ + at += strlen("\r\n\r\n"); + + /* Pause parsing so that if part of another packet + * is in queue then it doesn't get parsed, which + * may reset the parser state and cause current + * request packet to be lost */ + if (pause_parsing(parser, at) != ESP_OK) { + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + parser_data->last.at = 0; + parser_data->last.length = 0; + parser_data->status = PARSING_COMPLETE; + ESP_LOGD(TAG, LOG_FMT("message complete")); + return ESP_OK; +} + +static int read_block(httpd_req_t *req, size_t offset, size_t length) +{ + struct httpd_req_aux *raux = req->aux; + + /* Limits the read to scratch buffer size */ + size_t buf_len = MIN(length, (sizeof(raux->scratch) - offset)); + if (buf_len == 0) { + return 0; + } + + /* Receive data into buffer. If data is pending (from unrecv) then return + * immediately after receiving pending data, as pending data may just complete + * this request packet. */ + int nbytes = httpd_recv_with_opt(req, raux->scratch + offset, buf_len, true); + if (nbytes < 0) { + ESP_LOGD(TAG, LOG_FMT("error in httpd_recv")); + if (nbytes == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_err(req, HTTPD_408_REQ_TIMEOUT); + } + return -1; + } else if (nbytes == 0) { + ESP_LOGD(TAG, LOG_FMT("connection closed")); + return -1; + } + + ESP_LOGD(TAG, LOG_FMT("received HTTP request block size = %d"), nbytes); + return nbytes; +} + +static int parse_block(http_parser *parser, size_t offset, size_t length) +{ + parser_data_t *data = (parser_data_t *)(parser->data); + httpd_req_t *req = data->req; + struct httpd_req_aux *raux = req->aux; + size_t nparsed = 0; + + if (!length) { + ESP_LOGW(TAG, LOG_FMT("response uri/header too big")); + switch (data->status) { + case PARSING_URL: + data->error = HTTPD_414_URI_TOO_LONG; + break; + case PARSING_HDR_FIELD: + case PARSING_HDR_VALUE: + data->error = HTTPD_431_REQ_HDR_FIELDS_TOO_LARGE; + default: + break; + } + data->status = PARSING_FAILED; + return -1; + } + + /* Unpause the parsing if paused */ + if (data->paused) { + nparsed = continue_parsing(parser, length); + length -= nparsed; + offset += nparsed; + if (!length) { + return nparsed; + } + } + + /* Execute http_parser */ + nparsed = http_parser_execute(parser, &data->settings, + raux->scratch + offset, length); + + /* Check state */ + if (data->status == PARSING_FAILED) { + ESP_LOGW(TAG, LOG_FMT("parsing failed")); + return -1; + } else if (data->paused) { + /* Keep track of parsed data to be skipped + * during next parsing cycle */ + data->pre_parsed -= (length - nparsed); + return 0; + } else if (nparsed != length) { + /* http_parser error */ + data->status = PARSING_FAILED; + data->error = HTTPD_400_BAD_REQUEST; + ESP_LOGW(TAG, LOG_FMT("incomplete (%d/%d) with parser error = %d"), + nparsed, length, parser->http_errno); + return -1; + } + + /* Continue parsing this section of HTTP request packet */ + ESP_LOGD(TAG, LOG_FMT("parsed block size = %d"), offset + nparsed); + return offset + nparsed; +} + +static void parse_init(httpd_req_t *r, http_parser *parser, parser_data_t *data) +{ + /* Initialize parser data */ + memset(data, 0, sizeof(parser_data_t)); + data->req = r; + + /* Initialize parser */ + http_parser_init(parser, HTTP_REQUEST); + parser->data = (void *)data; + + /* Initialize parser settings */ + http_parser_settings_init(&data->settings); + + /* Set parser callbacks */ + data->settings.on_url = cb_url; + data->settings.on_header_field = cb_header_field; + data->settings.on_header_value = cb_header_value; + data->settings.on_headers_complete = cb_headers_complete; + data->settings.on_body = cb_on_body; + data->settings.on_message_complete = cb_no_body; +} + +/* Function that receives TCP data and runs parser on it + */ +static esp_err_t httpd_parse_req(struct httpd_data *hd) +{ + httpd_req_t *r = &hd->hd_req; + int blk_len, offset; + http_parser parser; + parser_data_t parser_data; + + /* Initialize parser */ + parse_init(r, &parser, &parser_data); + + /* Set offset to start of scratch buffer */ + offset = 0; + do { + /* Read block into scratch buffer */ + if ((blk_len = read_block(r, offset, PARSER_BLOCK_SIZE)) < 0) { + /* Return error to close socket */ + return ESP_FAIL; + } + + /* This is used by the callbacks to track + * data usage of the buffer */ + parser_data.raw_datalen = blk_len + offset; + + /* Parse data block from buffer */ + if ((offset = parse_block(&parser, offset, blk_len)) < 0) { + /* Server/Client error. Send error code as response status */ + return httpd_resp_send_err(r, parser_data.error); + } + } while (parser_data.status != PARSING_COMPLETE); + + ESP_LOGD(TAG, LOG_FMT("parsing complete")); + return httpd_uri(hd); +} + +static void init_req(httpd_req_t *r, httpd_config_t *config) +{ + r->handle = 0; + r->method = 0; + memset((char*)r->uri, 0, sizeof(r->uri)); + r->content_len = 0; + r->aux = 0; + r->user_ctx = 0; + r->sess_ctx = 0; + r->free_ctx = 0; +} + +static void init_req_aux(struct httpd_req_aux *ra, httpd_config_t *config) +{ + ra->sd = 0; + memset(ra->scratch, 0, sizeof(ra->scratch)); + ra->remaining_len = 0; + ra->status = 0; + ra->content_type = 0; + ra->first_chunk_sent = 0; + ra->req_hdrs_count = 0; + ra->resp_hdrs_count = 0; + memset(ra->resp_hdrs, 0, config->max_resp_headers * sizeof(struct resp_hdr)); +} + +static void httpd_req_cleanup(httpd_req_t *r) +{ + struct httpd_req_aux *ra = r->aux; + + /* Retrieve session info from the request into the socket database */ + if (ra->sd->ctx != r->sess_ctx) { + /* Free previous context */ + httpd_sess_free_ctx(ra->sd->ctx, ra->sd->free_ctx); + ra->sd->ctx = r->sess_ctx; + } + ra->sd->free_ctx = r->free_ctx; + + /* Clear out the request and request_aux structures */ + ra->sd = NULL; + r->handle = NULL; + r->aux = NULL; +} + +/* Function that processes incoming TCP data and + * updates the http request data httpd_req_t + */ +esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd) +{ + httpd_req_t *r = &hd->hd_req; + init_req(r, &hd->config); + init_req_aux(&hd->hd_req_aux, &hd->config); + r->handle = hd; + r->aux = &hd->hd_req_aux; + /* Associate the request to the socket */ + struct httpd_req_aux *ra = r->aux; + ra->sd = sd; + /* Set defaults */ + ra->status = (char *)HTTPD_200; + ra->content_type = (char *)HTTPD_TYPE_TEXT; + ra->first_chunk_sent = false; + /* Copy session info to the request */ + r->sess_ctx = sd->ctx; + r->free_ctx = sd->free_ctx; + /* Parse request */ + esp_err_t err = httpd_parse_req(hd); + if (err != ESP_OK) { + httpd_req_cleanup(r); + } + return err; +} + +/* Function that resets the http request data + */ +esp_err_t httpd_req_delete(struct httpd_data *hd) +{ + httpd_req_t *r = &hd->hd_req; + struct httpd_req_aux *ra = r->aux; + + /* Finish off reading any pending/leftover data */ + while (ra->remaining_len) { + /* Any length small enough not to overload the stack, but large + * enough to finish off the buffers fast + */ + char dummy[32]; + int recv_len = MIN(sizeof(dummy) - 1, ra->remaining_len); + int ret = httpd_req_recv(r, dummy, recv_len); + if (ret < 0) { + httpd_req_cleanup(r); + return ESP_FAIL; + } + + dummy[ret] = '\0'; + ESP_LOGD(TAG, LOG_FMT("purging data : %s"), dummy); + } + + httpd_req_cleanup(r); + return ESP_OK; +} + +/* Validates the request to prevent users from calling APIs, that are to + * be called only inside URI handler, outside the handler context + */ +bool httpd_validate_req_ptr(httpd_req_t *r) +{ + if (r) { + struct httpd_data *hd = (struct httpd_data *) r->handle; + if (hd) { + /* Check if this function is running in the context of + * the correct httpd server thread */ + if (httpd_os_thread_handle() == hd->hd_td.handle) { + return true; + } + } + } + return false; +} + +/* Helper function to get a URL query tag from a query string of the type param1=val1¶m2=val2 */ +esp_err_t httpd_query_key_value(const char *qry_str, const char *key, char *val, size_t val_size) +{ + if (qry_str == NULL || key == NULL || val == NULL) { + return ESP_ERR_INVALID_ARG; + } + + const char *qry_ptr = qry_str; + const size_t buf_len = val_size; + + while (strlen(qry_ptr)) { + /* Search for the '=' character. Else, it would mean + * that the parameter is invalid */ + const char *val_ptr = strchr(qry_ptr, '='); + if (!val_ptr) { + break; + } + size_t offset = val_ptr - qry_ptr; + + /* If the key, does not match, continue searching. + * Compare lengths first as key from url is not + * null terminated (has '=' in the end) */ + if ((offset != strlen(key)) || + (strncasecmp(qry_ptr, key, offset))) { + /* Get the name=val string. Multiple name=value pairs + * are separated by '&' */ + qry_ptr = strchr(val_ptr, '&'); + if (!qry_ptr) { + break; + } + qry_ptr++; + continue; + } + + /* Locate start of next query */ + qry_ptr = strchr(++val_ptr, '&'); + /* Or this could be the last query, in which + * case get to the end of query string */ + if (!qry_ptr) { + qry_ptr = val_ptr + strlen(val_ptr); + } + + /* Update value length, including one byte for null */ + val_size = qry_ptr - val_ptr + 1; + + /* Copy value to the caller's buffer. */ + strlcpy(val, val_ptr, MIN(val_size, buf_len)); + + /* If buffer length is smaller than needed, return truncation error */ + if (buf_len < val_size) { + return ESP_ERR_HTTPD_RESULT_TRUNC; + } + return ESP_OK; + } + ESP_LOGD(TAG, LOG_FMT("key %s not found"), key); + return ESP_ERR_NOT_FOUND; +} + +size_t httpd_req_get_url_query_len(httpd_req_t *r) +{ + if (r == NULL) { + return 0; + } + + if (!httpd_valid_req(r)) { + return 0; + } + + struct httpd_req_aux *ra = r->aux; + struct http_parser_url *res = &ra->url_parse_res; + + /* Check if query field is present in the URL */ + if (res->field_set & (1 << UF_QUERY)) { + return res->field_data[UF_QUERY].len; + } + return 0; +} + +esp_err_t httpd_req_get_url_query_str(httpd_req_t *r, char *buf, size_t buf_len) +{ + if (r == NULL || buf == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (!httpd_valid_req(r)) { + return ESP_ERR_HTTPD_INVALID_REQ; + } + + struct httpd_req_aux *ra = r->aux; + struct http_parser_url *res = &ra->url_parse_res; + + /* Check if query field is present in the URL */ + if (res->field_set & (1 << UF_QUERY)) { + const char *qry = r->uri + res->field_data[UF_QUERY].off; + + /* Minimum required buffer len for keeping + * null terminated query string */ + size_t min_buf_len = res->field_data[UF_QUERY].len + 1; + + strlcpy(buf, qry, MIN(buf_len, min_buf_len)); + if (buf_len < min_buf_len) { + return ESP_ERR_HTTPD_RESULT_TRUNC; + } + return ESP_OK; + } + return ESP_ERR_NOT_FOUND; +} + +/* Get the length of the value string of a header request field */ +size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *field) +{ + if (r == NULL || field == NULL) { + return 0; + } + + if (!httpd_valid_req(r)) { + return 0; + } + + struct httpd_req_aux *ra = r->aux; + const char *hdr_ptr = ra->scratch; /*!< Request headers are kept in scratch buffer */ + unsigned count = ra->req_hdrs_count; /*!< Count set during parsing */ + + while (count--) { + /* Search for the ':' character. Else, it would mean + * that the field is invalid + */ + const char *val_ptr = strchr(hdr_ptr, ':'); + if (!val_ptr) { + break; + } + + /* If the field, does not match, continue searching. + * Compare lengths first as field from header is not + * null terminated (has ':' in the end). + */ + if ((val_ptr - hdr_ptr != strlen(field)) || + (strncasecmp(hdr_ptr, field, strlen(field)))) { + hdr_ptr += strlen(hdr_ptr) + strlen("\r\n"); + continue; + } + /* Skip ':' */ + val_ptr++; + + /* Skip preceding space */ + while ((*val_ptr != '\0') && (*val_ptr == ' ')) { + val_ptr++; + } + return strlen(val_ptr); + } + return 0; +} + +/* Get the value of a field from the request headers */ +esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *r, const char *field, char *val, size_t val_size) +{ + if (r == NULL || field == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (!httpd_valid_req(r)) { + return ESP_ERR_HTTPD_INVALID_REQ; + } + + struct httpd_req_aux *ra = r->aux; + const char *hdr_ptr = ra->scratch; /*!< Request headers are kept in scratch buffer */ + unsigned count = ra->req_hdrs_count; /*!< Count set during parsing */ + const size_t buf_len = val_size; + + while (count--) { + /* Search for the ':' character. Else, it would mean + * that the field is invalid + */ + const char *val_ptr = strchr(hdr_ptr, ':'); + if (!val_ptr) { + break; + } + + /* If the field, does not match, continue searching. + * Compare lengths first as field from header is not + * null terminated (has ':' in the end). + */ + if ((val_ptr - hdr_ptr != strlen(field)) || + (strncasecmp(hdr_ptr, field, strlen(field)))) { + hdr_ptr += strlen(hdr_ptr) + strlen("\r\n"); + continue; + } + + /* Skip ':' */ + val_ptr++; + + /* Skip preceding space */ + while ((*val_ptr != '\0') && (*val_ptr == ' ')) { + val_ptr++; + } + + /* Get the NULL terminated value and copy it to the caller's buffer. */ + strlcpy(val, val_ptr, buf_len); + + /* Update value length, including one byte for null */ + val_size = strlen(val_ptr) + 1; + + /* If buffer length is smaller than needed, return truncation error */ + if (buf_len < val_size) { + return ESP_ERR_HTTPD_RESULT_TRUNC; + } + return ESP_OK; + } + return ESP_ERR_NOT_FOUND; +} diff --git a/components/esp_http_server/src/httpd_sess.c b/components/esp_http_server/src/httpd_sess.c new file mode 100644 index 00000000..8c0601fd --- /dev/null +++ b/components/esp_http_server/src/httpd_sess.c @@ -0,0 +1,386 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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 <stdlib.h> +#include <esp_log.h> +#include <esp_err.h> + +#include <esp_http_server.h> +#include "esp_httpd_priv.h" + +static const char *TAG = "httpd_sess"; + +bool httpd_is_sess_available(struct httpd_data *hd) +{ + int i; + for (i = 0; i < hd->config.max_open_sockets; i++) { + if (hd->hd_sd[i].fd == -1) { + return true; + } + } + return false; +} + +struct sock_db *httpd_sess_get(struct httpd_data *hd, int sockfd) +{ + if (hd == NULL) { + return NULL; + } + + /* Check if called inside a request handler, and the + * session sockfd in use is same as the parameter */ + if ((hd->hd_req_aux.sd) && (hd->hd_req_aux.sd->fd == sockfd)) { + /* Just return the pointer to the sock_db + * corresponding to the request */ + return hd->hd_req_aux.sd; + } + + int i; + for (i = 0; i < hd->config.max_open_sockets; i++) { + if (hd->hd_sd[i].fd == sockfd) { + return &hd->hd_sd[i]; + } + } + return NULL; +} + +esp_err_t httpd_sess_new(struct httpd_data *hd, int newfd) +{ + ESP_LOGD(TAG, LOG_FMT("fd = %d"), newfd); + + if (httpd_sess_get(hd, newfd)) { + ESP_LOGE(TAG, LOG_FMT("session already exists with fd = %d"), newfd); + return ESP_FAIL; + } + + int i; + for (i = 0; i < hd->config.max_open_sockets; i++) { + if (hd->hd_sd[i].fd == -1) { + memset(&hd->hd_sd[i], 0, sizeof(hd->hd_sd[i])); + hd->hd_sd[i].fd = newfd; + hd->hd_sd[i].handle = (httpd_handle_t) hd; + hd->hd_sd[i].send_fn = httpd_default_send; + hd->hd_sd[i].recv_fn = httpd_default_recv; + + /* Call user-defined session opening function */ + if (hd->config.open_fn) { + esp_err_t ret = hd->config.open_fn(hd, hd->hd_sd[i].fd); + if (ret != ESP_OK) return ret; + } + return ESP_OK; + } + } + ESP_LOGD(TAG, LOG_FMT("unable to launch session for fd = %d"), newfd); + return ESP_FAIL; +} + +void httpd_sess_free_ctx(void *ctx, httpd_free_ctx_fn_t free_fn) +{ + if (ctx) { + if (free_fn) { + free_fn(ctx); + } else { + free(ctx); + } + } +} + +void *httpd_sess_get_ctx(httpd_handle_t handle, int sockfd) +{ + struct sock_db *sd = httpd_sess_get(handle, sockfd); + if (sd == NULL) { + return NULL; + } + + /* Check if the function has been called from inside a + * request handler, in which case fetch the context from + * the httpd_req_t structure */ + struct httpd_data *hd = (struct httpd_data *) handle; + if (hd->hd_req_aux.sd == sd) { + return hd->hd_req.sess_ctx; + } + + return sd->ctx; +} + +void httpd_sess_set_ctx(httpd_handle_t handle, int sockfd, void *ctx, httpd_free_ctx_fn_t free_fn) +{ + struct sock_db *sd = httpd_sess_get(handle, sockfd); + if (sd == NULL) { + return; + } + + /* Check if the function has been called from inside a + * request handler, in which case set the context inside + * the httpd_req_t structure */ + struct httpd_data *hd = (struct httpd_data *) handle; + if (hd->hd_req_aux.sd == sd) { + if (hd->hd_req.sess_ctx != ctx) { + /* Don't free previous context if it is in sockdb + * as it will be freed inside httpd_req_cleanup() */ + if (sd->ctx != hd->hd_req.sess_ctx) { + /* Free previous context */ + httpd_sess_free_ctx(hd->hd_req.sess_ctx, hd->hd_req.free_ctx); + } + hd->hd_req.sess_ctx = ctx; + } + hd->hd_req.free_ctx = free_fn; + return; + } + + /* Else set the context inside the sock_db structure */ + if (sd->ctx != ctx) { + /* Free previous context */ + httpd_sess_free_ctx(sd->ctx, sd->free_ctx); + sd->ctx = ctx; + } + sd->free_ctx = free_fn; +} + +void *httpd_sess_get_transport_ctx(httpd_handle_t handle, int sockfd) +{ + struct sock_db *sd = httpd_sess_get(handle, sockfd); + if (sd == NULL) { + return NULL; + } + + return sd->transport_ctx; +} + +void httpd_sess_set_transport_ctx(httpd_handle_t handle, int sockfd, void *ctx, httpd_free_ctx_fn_t free_fn) +{ + struct sock_db *sd = httpd_sess_get(handle, sockfd); + if (sd == NULL) { + return; + } + + if (sd->transport_ctx != ctx) { + /* Free previous transport context */ + httpd_sess_free_ctx(sd->transport_ctx, sd->free_transport_ctx); + sd->transport_ctx = ctx; + } + sd->free_transport_ctx = free_fn; +} + +void httpd_sess_set_descriptors(struct httpd_data *hd, + fd_set *fdset, int *maxfd) +{ + int i; + *maxfd = -1; + for (i = 0; i < hd->config.max_open_sockets; i++) { + if (hd->hd_sd[i].fd != -1) { + FD_SET(hd->hd_sd[i].fd, fdset); + if (hd->hd_sd[i].fd > *maxfd) { + *maxfd = hd->hd_sd[i].fd; + } + } + } +} + +/** Check if a FD is valid */ +static int fd_is_valid(int fd) +{ + return fcntl(fd, F_GETFD) != -1 || errno != EBADF; +} + +void httpd_sess_delete_invalid(struct httpd_data *hd) +{ + for (int i = 0; i < hd->config.max_open_sockets; i++) { + if (hd->hd_sd[i].fd != -1 && !fd_is_valid(hd->hd_sd[i].fd)) { + ESP_LOGW(TAG, LOG_FMT("Closing invalid socket %d"), hd->hd_sd[i].fd); + httpd_sess_delete(hd, hd->hd_sd[i].fd); + } + } +} + +int httpd_sess_delete(struct httpd_data *hd, int fd) +{ + ESP_LOGD(TAG, LOG_FMT("fd = %d"), fd); + int i; + int pre_sess_fd = -1; + for (i = 0; i < hd->config.max_open_sockets; i++) { + if (hd->hd_sd[i].fd == fd) { + /* global close handler */ + if (hd->config.close_fn) { + hd->config.close_fn(hd, fd); + } + + /* release 'user' context */ + if (hd->hd_sd[i].ctx) { + if (hd->hd_sd[i].free_ctx) { + hd->hd_sd[i].free_ctx(hd->hd_sd[i].ctx); + } else { + free(hd->hd_sd[i].ctx); + } + hd->hd_sd[i].ctx = NULL; + hd->hd_sd[i].free_ctx = NULL; + } + + /* release 'transport' context */ + if (hd->hd_sd[i].transport_ctx) { + if (hd->hd_sd[i].free_transport_ctx) { + hd->hd_sd[i].free_transport_ctx(hd->hd_sd[i].transport_ctx); + } else { + free(hd->hd_sd[i].transport_ctx); + } + hd->hd_sd[i].transport_ctx = NULL; + hd->hd_sd[i].free_transport_ctx = NULL; + } + + /* mark session slot as available */ + hd->hd_sd[i].fd = -1; + break; + } else if (hd->hd_sd[i].fd != -1) { + /* Return the fd just preceding the one being + * deleted so that iterator can continue from + * the correct fd */ + pre_sess_fd = hd->hd_sd[i].fd; + } + } + return pre_sess_fd; +} + +void httpd_sess_init(struct httpd_data *hd) +{ + int i; + for (i = 0; i < hd->config.max_open_sockets; i++) { + hd->hd_sd[i].fd = -1; + hd->hd_sd[i].ctx = NULL; + } +} + +bool httpd_sess_pending(struct httpd_data *hd, int fd) +{ + struct sock_db *sd = httpd_sess_get(hd, fd); + if (! sd) { + return ESP_FAIL; + } + + if (sd->pending_fn) { + // test if there's any data to be read (besides read() function, which is handled by select() in the main httpd loop) + // this should check e.g. for the SSL data buffer + if (sd->pending_fn(hd, fd) > 0) return true; + } + + return (sd->pending_len != 0); +} + +/* This MUST return ESP_OK on successful execution. If any other + * value is returned, everything related to this socket will be + * cleaned up and the socket will be closed. + */ +esp_err_t httpd_sess_process(struct httpd_data *hd, int newfd) +{ + struct sock_db *sd = httpd_sess_get(hd, newfd); + if (! sd) { + return ESP_FAIL; + } + + ESP_LOGD(TAG, LOG_FMT("httpd_req_new")); + if (httpd_req_new(hd, sd) != ESP_OK) { + return ESP_FAIL; + } + ESP_LOGD(TAG, LOG_FMT("httpd_req_delete")); + if (httpd_req_delete(hd) != ESP_OK) { + return ESP_FAIL; + } + ESP_LOGD(TAG, LOG_FMT("success")); + sd->timestamp = httpd_os_get_timestamp(); + return ESP_OK; +} + +esp_err_t httpd_sess_update_timestamp(httpd_handle_t handle, int sockfd) +{ + if (handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + + /* Search for the socket database entry */ + struct httpd_data *hd = (struct httpd_data *) handle; + int i; + for (i = 0; i < hd->config.max_open_sockets; i++) { + if (hd->hd_sd[i].fd == sockfd) { + hd->hd_sd[i].timestamp = httpd_os_get_timestamp(); + return ESP_OK; + } + } + return ESP_ERR_NOT_FOUND; +} + +esp_err_t httpd_sess_close_lru(struct httpd_data *hd) +{ + int64_t timestamp = INT64_MAX; + int lru_fd = -1; + int i; + for (i = 0; i < hd->config.max_open_sockets; i++) { + /* If a descriptor is -1, there is no need to close any session. + * So, we can return from here, without finding the Least Recently Used + * session + */ + if (hd->hd_sd[i].fd == -1) { + return ESP_OK; + } + if (hd->hd_sd[i].timestamp < timestamp) { + timestamp = hd->hd_sd[i].timestamp; + lru_fd = hd->hd_sd[i].fd; + } + } + ESP_LOGD(TAG, LOG_FMT("fd = %d"), lru_fd); + return httpd_sess_trigger_close(hd, lru_fd); +} + +int httpd_sess_iterate(struct httpd_data *hd, int start_fd) +{ + int start_index = 0; + int i; + + if (start_fd != -1) { + /* Take our index to where this fd is stored */ + for (i = 0; i < hd->config.max_open_sockets; i++) { + if (hd->hd_sd[i].fd == start_fd) { + start_index = i + 1; + break; + } + } + } + + for (i = start_index; i < hd->config.max_open_sockets; i++) { + if (hd->hd_sd[i].fd != -1) { + return hd->hd_sd[i].fd; + } + } + return -1; +} + +static void httpd_sess_close(void *arg) +{ + struct sock_db *sock_db = (struct sock_db *)arg; + if (sock_db) { + int fd = sock_db->fd; + struct httpd_data *hd = (struct httpd_data *) sock_db->handle; + httpd_sess_delete(hd, fd); + close(fd); + } +} + +esp_err_t httpd_sess_trigger_close(httpd_handle_t handle, int sockfd) +{ + struct sock_db *sock_db = httpd_sess_get(handle, sockfd); + if (sock_db) { + return httpd_queue_work(handle, httpd_sess_close, sock_db); + } + + return ESP_ERR_NOT_FOUND; +} diff --git a/components/esp_http_server/src/httpd_txrx.c b/components/esp_http_server/src/httpd_txrx.c new file mode 100644 index 00000000..69a5ba00 --- /dev/null +++ b/components/esp_http_server/src/httpd_txrx.c @@ -0,0 +1,553 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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 <errno.h> +#include <esp_log.h> +#include <esp_err.h> + +#include <esp_http_server.h> +#include "esp_httpd_priv.h" + +static const char *TAG = "httpd_txrx"; + +esp_err_t httpd_sess_set_send_override(httpd_handle_t hd, int sockfd, httpd_send_func_t send_func) +{ + struct sock_db *sess = httpd_sess_get(hd, sockfd); + if (!sess) { + return ESP_ERR_INVALID_ARG; + } + sess->send_fn = send_func; + return ESP_OK; +} + +esp_err_t httpd_sess_set_recv_override(httpd_handle_t hd, int sockfd, httpd_recv_func_t recv_func) +{ + struct sock_db *sess = httpd_sess_get(hd, sockfd); + if (!sess) { + return ESP_ERR_INVALID_ARG; + } + sess->recv_fn = recv_func; + return ESP_OK; +} + +esp_err_t httpd_sess_set_pending_override(httpd_handle_t hd, int sockfd, httpd_pending_func_t pending_func) +{ + struct sock_db *sess = httpd_sess_get(hd, sockfd); + if (!sess) { + return ESP_ERR_INVALID_ARG; + } + sess->pending_fn = pending_func; + return ESP_OK; +} + +int httpd_send(httpd_req_t *r, const char *buf, size_t buf_len) +{ + if (r == NULL || buf == NULL) { + return HTTPD_SOCK_ERR_INVALID; + } + + if (!httpd_valid_req(r)) { + return HTTPD_SOCK_ERR_INVALID; + } + + struct httpd_req_aux *ra = r->aux; + int ret = ra->sd->send_fn(ra->sd->handle, ra->sd->fd, buf, buf_len, 0); + if (ret < 0) { + ESP_LOGD(TAG, LOG_FMT("error in send_fn")); + return ret; + } + return ret; +} + +static esp_err_t httpd_send_all(httpd_req_t *r, const char *buf, size_t buf_len) +{ + struct httpd_req_aux *ra = r->aux; + int ret; + + while (buf_len > 0) { + ret = ra->sd->send_fn(ra->sd->handle, ra->sd->fd, buf, buf_len, 0); + if (ret < 0) { + ESP_LOGD(TAG, LOG_FMT("error in send_fn")); + return ESP_FAIL; + } + ESP_LOGD(TAG, LOG_FMT("sent = %d"), ret); + buf += ret; + buf_len -= ret; + } + return ESP_OK; +} + +static size_t httpd_recv_pending(httpd_req_t *r, char *buf, size_t buf_len) +{ + struct httpd_req_aux *ra = r->aux; + size_t offset = sizeof(ra->sd->pending_data) - ra->sd->pending_len; + + /* buf_len must not be greater than remaining_len */ + buf_len = MIN(ra->sd->pending_len, buf_len); + memcpy(buf, ra->sd->pending_data + offset, buf_len); + + ra->sd->pending_len -= buf_len; + return buf_len; +} + +int httpd_recv_with_opt(httpd_req_t *r, char *buf, size_t buf_len, bool halt_after_pending) +{ + ESP_LOGD(TAG, LOG_FMT("requested length = %d"), buf_len); + + size_t pending_len = 0; + struct httpd_req_aux *ra = r->aux; + + /* First fetch pending data from local buffer */ + if (ra->sd->pending_len > 0) { + ESP_LOGD(TAG, LOG_FMT("pending length = %d"), ra->sd->pending_len); + pending_len = httpd_recv_pending(r, buf, buf_len); + buf += pending_len; + buf_len -= pending_len; + + /* If buffer filled then no need to recv. + * If asked to halt after receiving pending data then + * return with received length */ + if (!buf_len || halt_after_pending) { + return pending_len; + } + } + + /* Receive data of remaining length */ + int ret = ra->sd->recv_fn(ra->sd->handle, ra->sd->fd, buf, buf_len, 0); + if (ret < 0) { + ESP_LOGD(TAG, LOG_FMT("error in recv_fn")); + if ((ret == HTTPD_SOCK_ERR_TIMEOUT) && (pending_len != 0)) { + /* If recv() timeout occurred, but pending data is + * present, return length of pending data. + * This behavior is similar to that of socket recv() + * function, which, in case has only partially read the + * requested length, due to timeout, returns with read + * length, rather than error */ + return pending_len; + } + return ret; + } + + ESP_LOGD(TAG, LOG_FMT("received length = %d"), ret + pending_len); + return ret + pending_len; +} + +int httpd_recv(httpd_req_t *r, char *buf, size_t buf_len) +{ + return httpd_recv_with_opt(r, buf, buf_len, false); +} + +size_t httpd_unrecv(struct httpd_req *r, const char *buf, size_t buf_len) +{ + struct httpd_req_aux *ra = r->aux; + /* Truncate if external buf_len is greater than pending_data buffer size */ + ra->sd->pending_len = MIN(sizeof(ra->sd->pending_data), buf_len); + + /* Copy data into internal pending_data buffer */ + size_t offset = sizeof(ra->sd->pending_data) - ra->sd->pending_len; + memcpy(ra->sd->pending_data + offset, buf, buf_len); + ESP_LOGD(TAG, LOG_FMT("length = %d"), ra->sd->pending_len); + return ra->sd->pending_len; +} + +/** + * This API appends an additional header field-value pair in the HTTP response. + * But the header isn't sent out until any of the send APIs is executed. + */ +esp_err_t httpd_resp_set_hdr(httpd_req_t *r, const char *field, const char *value) +{ + if (r == NULL || field == NULL || value == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (!httpd_valid_req(r)) { + return ESP_ERR_HTTPD_INVALID_REQ; + } + + struct httpd_req_aux *ra = r->aux; + struct httpd_data *hd = (struct httpd_data *) r->handle; + + /* Number of additional headers is limited */ + if (ra->resp_hdrs_count >= hd->config.max_resp_headers) { + return ESP_ERR_HTTPD_RESP_HDR; + } + + /* Assign header field-value pair */ + ra->resp_hdrs[ra->resp_hdrs_count].field = field; + ra->resp_hdrs[ra->resp_hdrs_count].value = value; + ra->resp_hdrs_count++; + + ESP_LOGD(TAG, LOG_FMT("new header = %s: %s"), field, value); + return ESP_OK; +} + +/** + * This API sets the status of the HTTP response to the value specified. + * But the status isn't sent out until any of the send APIs is executed. + */ +esp_err_t httpd_resp_set_status(httpd_req_t *r, const char *status) +{ + if (r == NULL || status == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (!httpd_valid_req(r)) { + return ESP_ERR_HTTPD_INVALID_REQ; + } + + struct httpd_req_aux *ra = r->aux; + ra->status = (char *)status; + return ESP_OK; +} + +/** + * This API sets the method/type of the HTTP response to the value specified. + * But the method isn't sent out until any of the send APIs is executed. + */ +esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *type) +{ + if (r == NULL || type == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (!httpd_valid_req(r)) { + return ESP_ERR_HTTPD_INVALID_REQ; + } + + struct httpd_req_aux *ra = r->aux; + ra->content_type = (char *)type; + return ESP_OK; +} + +esp_err_t httpd_resp_send(httpd_req_t *r, const char *buf, ssize_t buf_len) +{ + if (r == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (!httpd_valid_req(r)) { + return ESP_ERR_HTTPD_INVALID_REQ; + } + + struct httpd_req_aux *ra = r->aux; + const char *httpd_hdr_str = "HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n"; + const char *colon_separator = ": "; + const char *cr_lf_seperator = "\r\n"; + + if (buf_len == -1) buf_len = strlen(buf); + + /* Request headers are no longer available */ + ra->req_hdrs_count = 0; + + /* Size of essential headers is limited by scratch buffer size */ + if (snprintf(ra->scratch, sizeof(ra->scratch), httpd_hdr_str, + ra->status, ra->content_type, buf_len) >= sizeof(ra->scratch)) { + return ESP_ERR_HTTPD_RESP_HDR; + } + + /* Sending essential headers */ + if (httpd_send_all(r, ra->scratch, strlen(ra->scratch)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + + /* Sending additional headers based on set_header */ + for (unsigned i = 0; i < ra->resp_hdrs_count; i++) { + /* Send header field */ + if (httpd_send_all(r, ra->resp_hdrs[i].field, strlen(ra->resp_hdrs[i].field)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + /* Send ': ' */ + if (httpd_send_all(r, colon_separator, strlen(colon_separator)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + /* Send header value */ + if (httpd_send_all(r, ra->resp_hdrs[i].value, strlen(ra->resp_hdrs[i].value)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + /* Send CR + LF */ + if (httpd_send_all(r, cr_lf_seperator, strlen(cr_lf_seperator)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + } + + /* End header section */ + if (httpd_send_all(r, cr_lf_seperator, strlen(cr_lf_seperator)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + + /* Sending content */ + if (buf && buf_len) { + if (httpd_send_all(r, buf, buf_len) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + } + return ESP_OK; +} + +esp_err_t httpd_resp_send_chunk(httpd_req_t *r, const char *buf, ssize_t buf_len) +{ + if (r == NULL) { + return ESP_ERR_INVALID_ARG; + } + + if (!httpd_valid_req(r)) { + return ESP_ERR_HTTPD_INVALID_REQ; + } + + if (buf_len == -1) buf_len = strlen(buf); + + struct httpd_req_aux *ra = r->aux; + const char *httpd_chunked_hdr_str = "HTTP/1.1 %s\r\nContent-Type: %s\r\nTransfer-Encoding: chunked\r\n"; + const char *colon_separator = ": "; + const char *cr_lf_seperator = "\r\n"; + + /* Request headers are no longer available */ + ra->req_hdrs_count = 0; + + if (!ra->first_chunk_sent) { + /* Size of essential headers is limited by scratch buffer size */ + if (snprintf(ra->scratch, sizeof(ra->scratch), httpd_chunked_hdr_str, + ra->status, ra->content_type) >= sizeof(ra->scratch)) { + return ESP_ERR_HTTPD_RESP_HDR; + } + + /* Sending essential headers */ + if (httpd_send_all(r, ra->scratch, strlen(ra->scratch)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + + /* Sending additional headers based on set_header */ + for (unsigned i = 0; i < ra->resp_hdrs_count; i++) { + /* Send header field */ + if (httpd_send_all(r, ra->resp_hdrs[i].field, strlen(ra->resp_hdrs[i].field)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + /* Send ': ' */ + if (httpd_send_all(r, colon_separator, strlen(colon_separator)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + /* Send header value */ + if (httpd_send_all(r, ra->resp_hdrs[i].value, strlen(ra->resp_hdrs[i].value)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + /* Send CR + LF */ + if (httpd_send_all(r, cr_lf_seperator, strlen(cr_lf_seperator)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + } + + /* End header section */ + if (httpd_send_all(r, cr_lf_seperator, strlen(cr_lf_seperator)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + ra->first_chunk_sent = true; + } + + /* Sending chunked content */ + char len_str[10]; + snprintf(len_str, sizeof(len_str), "%x\r\n", buf_len); + if (httpd_send_all(r, len_str, strlen(len_str)) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + + if (buf) { + if (httpd_send_all(r, buf, (size_t) buf_len) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + } + + /* Indicate end of chunk */ + if (httpd_send_all(r, "\r\n", strlen("\r\n")) != ESP_OK) { + return ESP_ERR_HTTPD_RESP_SEND; + } + return ESP_OK; +} + +esp_err_t httpd_resp_send_404(httpd_req_t *r) +{ + return httpd_resp_send_err(r, HTTPD_404_NOT_FOUND); +} + +esp_err_t httpd_resp_send_408(httpd_req_t *r) +{ + return httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT); +} + +esp_err_t httpd_resp_send_500(httpd_req_t *r) +{ + return httpd_resp_send_err(r, HTTPD_500_SERVER_ERROR); +} + +esp_err_t httpd_resp_send_err(httpd_req_t *req, httpd_err_resp_t error) +{ + const char *msg; + const char *status; + switch (error) { + case HTTPD_501_METHOD_NOT_IMPLEMENTED: + status = "501 Method Not Implemented"; + msg = "Request method is not supported by server"; + break; + case HTTPD_505_VERSION_NOT_SUPPORTED: + status = "505 Version Not Supported"; + msg = "HTTP version not supported by server"; + break; + case HTTPD_400_BAD_REQUEST: + status = "400 Bad Request"; + msg = "Server unable to understand request due to invalid syntax"; + break; + case HTTPD_404_NOT_FOUND: + status = "404 Not Found"; + msg = "This URI doesn't exist"; + break; + case HTTPD_405_METHOD_NOT_ALLOWED: + status = "405 Method Not Allowed"; + msg = "Request method for this URI is not handled by server"; + break; + case HTTPD_408_REQ_TIMEOUT: + status = "408 Request Timeout"; + msg = "Server closed this connection"; + break; + case HTTPD_414_URI_TOO_LONG: + status = "414 URI Too Long"; + msg = "URI is too long for server to interpret"; + break; + case HTTPD_411_LENGTH_REQUIRED: + status = "411 Length Required"; + msg = "Chunked encoding not supported by server"; + break; + case HTTPD_431_REQ_HDR_FIELDS_TOO_LARGE: + status = "431 Request Header Fields Too Large"; + msg = "Header fields are too long for server to interpret"; + break; + case HTTPD_XXX_UPGRADE_NOT_SUPPORTED: + /* If the server does not support upgrade, or is unable to upgrade + * it responds with a standard HTTP/1.1 response */ + status = "200 OK"; + msg = "Upgrade not supported by server"; + break; + case HTTPD_500_SERVER_ERROR: + default: + status = "500 Server Error"; + msg = "Server has encountered an unexpected error"; + } + ESP_LOGW(TAG, LOG_FMT("%s - %s"), status, msg); + + httpd_resp_set_status (req, status); + httpd_resp_set_type (req, HTTPD_TYPE_TEXT); + return httpd_resp_send (req, msg, strlen(msg)); +} + +int httpd_req_recv(httpd_req_t *r, char *buf, size_t buf_len) +{ + if (r == NULL || buf == NULL) { + return HTTPD_SOCK_ERR_INVALID; + } + + if (!httpd_valid_req(r)) { + ESP_LOGW(TAG, LOG_FMT("invalid request")); + return HTTPD_SOCK_ERR_INVALID; + } + + struct httpd_req_aux *ra = r->aux; + ESP_LOGD(TAG, LOG_FMT("remaining length = %d"), ra->remaining_len); + + if (buf_len > ra->remaining_len) { + buf_len = ra->remaining_len; + } + if (buf_len == 0) { + return buf_len; + } + + int ret = httpd_recv(r, buf, buf_len); + if (ret < 0) { + ESP_LOGD(TAG, LOG_FMT("error in httpd_recv")); + return ret; + } + ra->remaining_len -= ret; + ESP_LOGD(TAG, LOG_FMT("received length = %d"), ret); + return ret; +} + +int httpd_req_to_sockfd(httpd_req_t *r) +{ + if (r == NULL) { + return -1; + } + + if (!httpd_valid_req(r)) { + ESP_LOGW(TAG, LOG_FMT("invalid request")); + return -1; + } + + struct httpd_req_aux *ra = r->aux; + return ra->sd->fd; +} + +static int httpd_sock_err(const char *ctx, int sockfd) +{ + int errval; + int sock_err; + size_t sock_err_len = sizeof(sock_err); + + if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &sock_err, &sock_err_len) < 0) { + ESP_LOGE(TAG, LOG_FMT("error calling getsockopt : %d"), errno); + return HTTPD_SOCK_ERR_FAIL; + } + ESP_LOGW(TAG, LOG_FMT("error in %s : %d"), ctx, sock_err); + + switch(sock_err) { + case EAGAIN: + case EINTR: + errval = HTTPD_SOCK_ERR_TIMEOUT; + break; + case EINVAL: + case EBADF: + case EFAULT: + case ENOTSOCK: + errval = HTTPD_SOCK_ERR_INVALID; + break; + default: + errval = HTTPD_SOCK_ERR_FAIL; + } + return errval; +} + +int httpd_default_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) +{ + (void)hd; + if (buf == NULL) { + return HTTPD_SOCK_ERR_INVALID; + } + + int ret = send(sockfd, buf, buf_len, flags); + if (ret < 0) { + return httpd_sock_err("send", sockfd); + } + return ret; +} + +int httpd_default_recv(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, int flags) +{ + (void)hd; + if (buf == NULL) { + return HTTPD_SOCK_ERR_INVALID; + } + + int ret = recv(sockfd, buf, buf_len, flags); + if (ret < 0) { + return httpd_sock_err("recv", sockfd); + } + return ret; +} diff --git a/components/esp_http_server/src/httpd_uri.c b/components/esp_http_server/src/httpd_uri.c new file mode 100644 index 00000000..e31a6108 --- /dev/null +++ b/components/esp_http_server/src/httpd_uri.c @@ -0,0 +1,224 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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 <errno.h> +#include <esp_log.h> +#include <esp_err.h> +#include <http_parser.h> + +#include <esp_http_server.h> +#include "esp_httpd_priv.h" + +static const char *TAG = "httpd_uri"; + +static int httpd_find_uri_handler(struct httpd_data *hd, + const char* uri, + httpd_method_t method) +{ + for (int i = 0; i < hd->config.max_uri_handlers; i++) { + if (hd->hd_calls[i]) { + ESP_LOGD(TAG, LOG_FMT("[%d] = %s"), i, hd->hd_calls[i]->uri); + if ((hd->hd_calls[i]->method == method) && // First match methods + (strcmp(hd->hd_calls[i]->uri, uri) == 0)) { // Then match uri strings + return i; + } + } + } + return -1; +} + +esp_err_t httpd_register_uri_handler(httpd_handle_t handle, + const httpd_uri_t *uri_handler) +{ + if (handle == NULL || uri_handler == NULL) { + return ESP_ERR_INVALID_ARG; + } + + struct httpd_data *hd = (struct httpd_data *) handle; + + /* Make sure another handler with same URI and method + * is not already registered + */ + if (httpd_find_uri_handler(handle, uri_handler->uri, + uri_handler->method) != -1) { + ESP_LOGW(TAG, LOG_FMT("handler %s with method %d already registered"), + uri_handler->uri, uri_handler->method); + return ESP_ERR_HTTPD_HANDLER_EXISTS; + } + + for (int i = 0; i < hd->config.max_uri_handlers; i++) { + if (hd->hd_calls[i] == NULL) { + hd->hd_calls[i] = malloc(sizeof(httpd_uri_t)); + if (hd->hd_calls[i] == NULL) { + /* Failed to allocate memory */ + return ESP_ERR_HTTPD_ALLOC_MEM; + } + + /* Copy URI string */ + hd->hd_calls[i]->uri = strdup(uri_handler->uri); + if (hd->hd_calls[i]->uri == NULL) { + /* Failed to allocate memory */ + free(hd->hd_calls[i]); + return ESP_ERR_HTTPD_ALLOC_MEM; + } + + /* Copy remaining members */ + hd->hd_calls[i]->method = uri_handler->method; + hd->hd_calls[i]->handler = uri_handler->handler; + hd->hd_calls[i]->user_ctx = uri_handler->user_ctx; + ESP_LOGD(TAG, LOG_FMT("[%d] installed %s"), i, uri_handler->uri); + return ESP_OK; + } + ESP_LOGD(TAG, LOG_FMT("[%d] exists %s"), i, hd->hd_calls[i]->uri); + } + ESP_LOGW(TAG, LOG_FMT("no slots left for registering handler")); + return ESP_ERR_HTTPD_HANDLERS_FULL; +} + +esp_err_t httpd_unregister_uri_handler(httpd_handle_t handle, + const char *uri, httpd_method_t method) +{ + if (handle == NULL || uri == NULL) { + return ESP_ERR_INVALID_ARG; + } + + struct httpd_data *hd = (struct httpd_data *) handle; + int i = httpd_find_uri_handler(hd, uri, method); + + if (i != -1) { + ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, hd->hd_calls[i]->uri); + + free((char*)hd->hd_calls[i]->uri); + free(hd->hd_calls[i]); + hd->hd_calls[i] = NULL; + return ESP_OK; + } + ESP_LOGW(TAG, LOG_FMT("handler %s with method %d not found"), uri, method); + return ESP_ERR_NOT_FOUND; +} + +esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char *uri) +{ + if (handle == NULL || uri == NULL) { + return ESP_ERR_INVALID_ARG; + } + + struct httpd_data *hd = (struct httpd_data *) handle; + bool found = false; + + for (int i = 0; i < hd->config.max_uri_handlers; i++) { + if ((hd->hd_calls[i] != NULL) && + (strcmp(hd->hd_calls[i]->uri, uri) == 0)) { + ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, uri); + + free((char*)hd->hd_calls[i]->uri); + free(hd->hd_calls[i]); + hd->hd_calls[i] = NULL; + found = true; + } + } + if (!found) { + ESP_LOGW(TAG, LOG_FMT("no handler found for URI %s"), uri); + } + return (found ? ESP_OK : ESP_ERR_NOT_FOUND); +} + +void httpd_unregister_all_uri_handlers(struct httpd_data *hd) +{ + for (unsigned i = 0; i < hd->config.max_uri_handlers; i++) { + if (hd->hd_calls[i]) { + ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, hd->hd_calls[i]->uri); + + free((char*)hd->hd_calls[i]->uri); + free(hd->hd_calls[i]); + } + } +} + +/* Alternate implmentation of httpd_find_uri_handler() + * which takes a uri_len field. This is useful when the URI + * string contains extra parameters that are not to be included + * while matching with the registered URI_handler strings + */ +static httpd_uri_t* httpd_find_uri_handler2(httpd_err_resp_t *err, + struct httpd_data *hd, + const char *uri, size_t uri_len, + httpd_method_t method) +{ + *err = 0; + for (int i = 0; i < hd->config.max_uri_handlers; i++) { + if (hd->hd_calls[i]) { + ESP_LOGD(TAG, LOG_FMT("[%d] = %s"), i, hd->hd_calls[i]->uri); + if ((strlen(hd->hd_calls[i]->uri) == uri_len) && // First match uri length + (strncmp(hd->hd_calls[i]->uri, uri, uri_len) == 0)) { // Then match uri strings + if (hd->hd_calls[i]->method == method) { // Finally match methods + return hd->hd_calls[i]; + } + /* URI found but method not allowed. + * If URI IS found later then this + * error is to be neglected */ + *err = HTTPD_405_METHOD_NOT_ALLOWED; + } + } + } + if (*err == 0) { + *err = HTTPD_404_NOT_FOUND; + } + return NULL; +} + +esp_err_t httpd_uri(struct httpd_data *hd) +{ + httpd_uri_t *uri = NULL; + httpd_req_t *req = &hd->hd_req; + struct http_parser_url *res = &hd->hd_req_aux.url_parse_res; + + /* For conveying URI not found/method not allowed */ + httpd_err_resp_t err = 0; + + ESP_LOGD(TAG, LOG_FMT("request for %s with type %d"), req->uri, req->method); + /* URL parser result contains offset and length of path string */ + if (res->field_set & (1 << UF_PATH)) { + uri = httpd_find_uri_handler2(&err, hd, + req->uri + res->field_data[UF_PATH].off, + res->field_data[UF_PATH].len, + req->method); + } + + /* If URI with method not found, respond with error code */ + if (uri == NULL) { + switch (err) { + case HTTPD_404_NOT_FOUND: + ESP_LOGW(TAG, LOG_FMT("URI '%s' not found"), req->uri); + return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND); + case HTTPD_405_METHOD_NOT_ALLOWED: + ESP_LOGW(TAG, LOG_FMT("Method '%d' not allowed for URI '%s'"), req->method, req->uri); + return httpd_resp_send_err(req, HTTPD_405_METHOD_NOT_ALLOWED); + default: + return ESP_FAIL; + } + } + + /* Attach user context data (passed during URI registration) into request */ + req->user_ctx = uri->user_ctx; + + /* Invoke handler */ + if (uri->handler(req) != ESP_OK) { + /* Handler returns error, this socket should be closed */ + ESP_LOGW(TAG, LOG_FMT("uri handler execution failed")); + return ESP_FAIL; + } + return ESP_OK; +} diff --git a/components/esp_http_server/src/port/esp32/osal.h b/components/esp_http_server/src/port/esp32/osal.h new file mode 100644 index 00000000..d4e76fcd --- /dev/null +++ b/components/esp_http_server/src/port/esp32/osal.h @@ -0,0 +1,69 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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 _OSAL_H_ +#define _OSAL_H_ + +#include <freertos/FreeRTOS.h> +#include <freertos/task.h> +#include <unistd.h> +#include <stdint.h> +#include <esp_timer.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#define OS_SUCCESS ESP_OK +#define OS_FAIL ESP_FAIL + +typedef TaskHandle_t othread_t; + +static inline int httpd_os_thread_create(othread_t *thread, + const char *name, uint16_t stacksize, int prio, + void (*thread_routine)(void *arg), void *arg) +{ + int ret = xTaskCreate(thread_routine, name, stacksize, arg, prio, thread); + if (ret == pdPASS) { + return OS_SUCCESS; + } + return OS_FAIL; +} + +/* Only self delete is supported */ +static inline void httpd_os_thread_delete() +{ + vTaskDelete(xTaskGetCurrentTaskHandle()); +} + +static inline void httpd_os_thread_sleep(int msecs) +{ + vTaskDelay(msecs / portTICK_RATE_MS); +} + +static inline int64_t httpd_os_get_timestamp() +{ + return esp_timer_get_time(); +} + +static inline othread_t httpd_os_thread_handle() +{ + return xTaskGetCurrentTaskHandle(); +} + +#ifdef __cplusplus +} +#endif + +#endif /* ! _OSAL_H_ */ diff --git a/components/esp_http_server/src/util/ctrl_sock.c b/components/esp_http_server/src/util/ctrl_sock.c new file mode 100644 index 00000000..5c84f80e --- /dev/null +++ b/components/esp_http_server/src/util/ctrl_sock.c @@ -0,0 +1,77 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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 <string.h> +#include <unistd.h> + +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> + +#include "ctrl_sock.h" + +/* Control socket, because in some network stacks select can't be woken up any + * other way + */ +int cs_create_ctrl_sock(int port) +{ + int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (fd < 0) { + return -1; + } + + int ret; + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + inet_aton("127.0.0.1", &addr.sin_addr); + ret = bind(fd, (struct sockaddr *)&addr, sizeof(addr)); + if (ret < 0) { + close(fd); + return -1; + } + return fd; +} + +void cs_free_ctrl_sock(int fd) +{ + close(fd); +} + +int cs_send_to_ctrl_sock(int send_fd, int port, void *data, unsigned int data_len) +{ + int ret; + struct sockaddr_in to_addr; + to_addr.sin_family = AF_INET; + to_addr.sin_port = htons(port); + inet_aton("127.0.0.1", &to_addr.sin_addr); + ret = sendto(send_fd, data, data_len, 0, (struct sockaddr *)&to_addr, sizeof(to_addr)); + + if (ret < 0) { + return -1; + } + return ret; +} + +int cs_recv_from_ctrl_sock(int fd, void *data, unsigned int data_len) +{ + int ret; + ret = recvfrom(fd, data, data_len, 0, NULL, NULL); + + if (ret < 0) { + return -1; + } + return ret; +} diff --git a/components/esp_http_server/src/util/ctrl_sock.h b/components/esp_http_server/src/util/ctrl_sock.h new file mode 100644 index 00000000..d392d353 --- /dev/null +++ b/components/esp_http_server/src/util/ctrl_sock.h @@ -0,0 +1,99 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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. + +/** + * \file ctrl_sock.h + * \brief Control Socket for select() wakeup + * + * LWIP doesn't allow an easy mechanism to on-demand wakeup a thread + * sleeping on select. This is a common requirement for sending + * control commands to a network server. This control socket API + * facilitates the same. + */ +#ifndef _CTRL_SOCK_H_ +#define _CTRL_SOCK_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Create a control socket + * + * LWIP doesn't allow an easy mechanism to on-demand wakeup a thread + * sleeping on select. This is a common requirement for sending + * control commands to a network server. This control socket API + * facilitates the same. + * + * This API will create a UDP control socket on the specified port. It + * will return a socket descriptor that can then be added to your + * fd_set in select() + * + * @param[in] port the local port on which the control socket will listen + * + * @return - the socket descriptor that can be added to the fd_set in select. + * - an error code if less than zero + */ +int cs_create_ctrl_sock(int port); + +/** + * @brief Free the control socket + * + * This frees up the control socket that was earlier created using + * cs_create_ctrl_sock() + * + * @param[in] fd the socket descriptor associated with this control socket + */ +void cs_free_ctrl_sock(int fd); + +/** + * @brief Send data to control socket + * + * This API sends data to the control socket. If a server is blocked + * on select() with the control socket, this call will wake up that + * server. + * + * @param[in] send_fd the socket for sending ctrl messages + * @param[in] port the port on which the control socket was created + * @param[in] data pointer to a buffer that contains data to send on the socket + * @param[in] data_len the length of the data contained in the buffer pointed to be data + * + * @return - the number of bytes sent to the control socket + * - an error code if less than zero + */ +int cs_send_to_ctrl_sock(int send_fd, int port, void *data, unsigned int data_len); + +/** + * @brief Receive data from control socket + * + * This API receives any data that was sent to the control + * socket. This will be typically called from the server thread to + * process any commands on this socket. + * + * @param[in] fd the socket descriptor of the control socket + * @param[in] data pointer to a buffer that will be used to store + * received from the control socket + * @param[in] data_len the maximum length of the data that can be + * stored in the buffer pointed by data + * + * @return - the number of bytes received from the control socket + * - an error code if less than zero + */ +int cs_recv_from_ctrl_sock(int fd, void *data, unsigned int data_len); + +#ifdef __cplusplus +} +#endif + +#endif /* ! _CTRL_SOCK_H_ */ diff --git a/components/esp_http_server/test/CMakeLists.txt b/components/esp_http_server/test/CMakeLists.txt new file mode 100644 index 00000000..7587213b --- /dev/null +++ b/components/esp_http_server/test/CMakeLists.txt @@ -0,0 +1,6 @@ +set(COMPONENT_SRCDIRS ".") +set(COMPONENT_ADD_INCLUDEDIRS ".") + +set(COMPONENT_REQUIRES unity esp_http_server) + +register_component() diff --git a/components/esp_http_server/test/component.mk b/components/esp_http_server/test/component.mk new file mode 100644 index 00000000..ce464a21 --- /dev/null +++ b/components/esp_http_server/test/component.mk @@ -0,0 +1 @@ +COMPONENT_ADD_LDFLAGS = -Wl,--whole-archive -l$(COMPONENT_NAME) -Wl,--no-whole-archive diff --git a/components/esp_http_server/test/test_http_server.c b/components/esp_http_server/test/test_http_server.c new file mode 100644 index 00000000..e343c99c --- /dev/null +++ b/components/esp_http_server/test/test_http_server.c @@ -0,0 +1,169 @@ +// Copyright 2018 Espressif Systems (Shanghai) PTE LTD +// +// 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 <stdlib.h> +#include <stdbool.h> +#include <esp_system.h> +#include <esp_http_server.h> + +#include "unity.h" +#include "test_utils.h" + +int pre_start_mem, post_stop_mem, post_stop_min_mem; +bool basic_sanity = true; + +esp_err_t null_func(httpd_req_t *req) +{ + return ESP_OK; +} + +httpd_uri_t handler_limit_uri (char* path) +{ + httpd_uri_t uri = { + .uri = path, + .method = HTTP_GET, + .handler = null_func, + .user_ctx = NULL, + }; + return uri; +}; + +static inline unsigned num_digits(unsigned x) +{ + unsigned digits = 1; + while ((x = x/10) != 0) { + digits++; + } + return digits; +} + +#define HTTPD_TEST_MAX_URI_HANDLERS 8 + +void test_handler_limit(httpd_handle_t hd) +{ + int i; + char x[HTTPD_TEST_MAX_URI_HANDLERS+1][num_digits(HTTPD_TEST_MAX_URI_HANDLERS)+1]; + httpd_uri_t uris[HTTPD_TEST_MAX_URI_HANDLERS+1]; + + for (i = 0; i < HTTPD_TEST_MAX_URI_HANDLERS + 1; i++) { + sprintf(x[i],"%d",i); + uris[i] = handler_limit_uri(x[i]); + } + + /* Register multiple instances of the same handler for MAX URI Handlers */ + for (i = 0; i < HTTPD_TEST_MAX_URI_HANDLERS; i++) { + TEST_ASSERT(httpd_register_uri_handler(hd, &uris[i]) == ESP_OK); + } + + /* Register the MAX URI + 1 Handlers should fail */ + TEST_ASSERT(httpd_register_uri_handler(hd, &uris[HTTPD_TEST_MAX_URI_HANDLERS]) != ESP_OK); + + /* Unregister the one of the Handler should pass */ + TEST_ASSERT(httpd_unregister_uri_handler(hd, uris[0].uri, uris[0].method) == ESP_OK); + + /* Unregister non added Handler should fail */ + TEST_ASSERT(httpd_unregister_uri_handler(hd, uris[0].uri, uris[0].method) != ESP_OK); + + /* Register the MAX URI Handler should pass */ + TEST_ASSERT(httpd_register_uri_handler(hd, &uris[0]) == ESP_OK); + + /* Reregister same instance of handler should fail */ + TEST_ASSERT(httpd_register_uri_handler(hd, &uris[0]) != ESP_OK); + + /* Register the MAX URI + 1 Handlers should fail */ + TEST_ASSERT(httpd_register_uri_handler(hd, &uris[HTTPD_TEST_MAX_URI_HANDLERS]) != ESP_OK); + + /* Unregister the same handler for MAX URI Handlers */ + for (i = 0; i < HTTPD_TEST_MAX_URI_HANDLERS; i++) { + TEST_ASSERT(httpd_unregister_uri_handler(hd, uris[i].uri, uris[i].method) == ESP_OK); + } + basic_sanity = false; +} + +/********************* Test Handler Limit End *******************/ + +httpd_handle_t test_httpd_start(uint16_t id) +{ + httpd_handle_t hd; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.max_uri_handlers = HTTPD_TEST_MAX_URI_HANDLERS; + config.server_port += id; + config.ctrl_port += id; + TEST_ASSERT(httpd_start(&hd, &config) == ESP_OK) + return hd; +} + +#define SERVER_INSTANCES 2 + +/* Currently this only tests for the number of tasks. + * Heap leakage is not tested as LWIP allocates memory + * which may not be freed immedietly causing erroneous + * evaluation. Another test to implement would be the + * monitoring of open sockets, but LWIP presently provides + * no such API for getting the number of open sockets. + */ +TEST_CASE("Leak Test", "[HTTP SERVER]") +{ + httpd_handle_t hd[SERVER_INSTANCES]; + unsigned task_count; + bool res = true; + + test_case_uses_tcpip(); + + task_count = uxTaskGetNumberOfTasks(); + printf("Initial task count: %d\n", task_count); + + pre_start_mem = esp_get_free_heap_size(); + + for (int i = 0; i < SERVER_INSTANCES; i++) { + hd[i] = test_httpd_start(i); + vTaskDelay(10); + unsigned num_tasks = uxTaskGetNumberOfTasks(); + task_count++; + if (num_tasks != task_count) { + printf("Incorrect task count (starting): %d expected %d\n", + num_tasks, task_count); + res = false; + } + } + + for (int i = 0; i < SERVER_INSTANCES; i++) { + if (httpd_stop(hd[i]) != ESP_OK) { + printf("Failed to stop httpd task %d\n", i); + res = false; + } + vTaskDelay(10); + unsigned num_tasks = uxTaskGetNumberOfTasks(); + task_count--; + if (num_tasks != task_count) { + printf("Incorrect task count (stopping): %d expected %d\n", + num_tasks, task_count); + res = false; + } + } + post_stop_mem = esp_get_free_heap_size(); + TEST_ASSERT(res == true); +} + +TEST_CASE("Basic Functionality Tests", "[HTTP SERVER]") +{ + httpd_handle_t hd; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + test_case_uses_tcpip(); + + TEST_ASSERT(httpd_start(&hd, &config) == ESP_OK); + test_handler_limit(hd); + TEST_ASSERT(httpd_stop(hd) == ESP_OK); +} diff --git a/examples/protocols/http_server/advanced_tests/CMakeLists.txt b/examples/protocols/http_server/advanced_tests/CMakeLists.txt new file mode 100644 index 00000000..b6f65f8f --- /dev/null +++ b/examples/protocols/http_server/advanced_tests/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(tests) + +target_include_directories(tests.elf PRIVATE main/include) diff --git a/examples/protocols/http_server/advanced_tests/Makefile b/examples/protocols/http_server/advanced_tests/Makefile new file mode 100644 index 00000000..178ddf69 --- /dev/null +++ b/examples/protocols/http_server/advanced_tests/Makefile @@ -0,0 +1,9 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := tests + +include $(IDF_PATH)/make/project.mk + diff --git a/examples/protocols/http_server/advanced_tests/http_server_advanced_test.py b/examples/protocols/http_server/advanced_tests/http_server_advanced_test.py new file mode 100644 index 00000000..0294bed9 --- /dev/null +++ b/examples/protocols/http_server/advanced_tests/http_server_advanced_test.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# +# Copyright 2018 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +import imp +import re +import os +import sys +import string +import random +import socket + +# This environment variable is expected on the host machine +test_fw_path = os.getenv("TEST_FW_PATH") +if test_fw_path and test_fw_path not in sys.path: + sys.path.insert(0, test_fw_path) + +# When running on local machine execute the following before running this script +# > make app bootloader +# > make print_flash_cmd | tail -n 1 > build/download.config +# > export TEST_FW_PATH=~/esp/esp-idf/tools/tiny-test-fw + +import TinyFW +import IDF +import Utility + +# Import client module +expath = os.path.dirname(os.path.realpath(__file__)) +client = imp.load_source("client", expath + "/scripts/test.py") + +# Due to connectivity issues (between runner host and DUT) in the runner environment, +# some of the `advanced_tests` are ignored. These tests are intended for verifying +# the expected limits of the http_server capabilities, and implement sending and receiving +# of large HTTP packets and malformed requests, running multiple parallel sessions, etc. +# It is advised that all these tests be run locally, when making changes or adding new +# features to this component. +@IDF.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_http_server_advanced(env, extra_data): + # Acquire DUT + dut1 = env.get_dut("http_server", "examples/protocols/http_server/advanced_tests") + + # Get binary file + binary_file = os.path.join(dut1.app.binary_path, "tests.bin") + bin_size = os.path.getsize(binary_file) + IDF.log_performance("http_server_bin_size", "{}KB".format(bin_size//1024)) + IDF.check_performance("http_server_bin_size", bin_size//1024) + + # Upload binary and start testing + Utility.console_log("Starting http_server advanced test app") + dut1.start_app() + + # Parse IP address of STA + Utility.console_log("Waiting to connect with AP") + got_ip = dut1.expect(re.compile(r"(?:[\s\S]*)Got IP: '(\d+.\d+.\d+.\d+)'"), timeout=30)[0] + + got_port = dut1.expect(re.compile(r"(?:[\s\S]*)Started HTTP server on port: '(\d+)'"), timeout=15)[0] + result = dut1.expect(re.compile(r"(?:[\s\S]*)Max URI handlers: '(\d+)'(?:[\s\S]*)Max Open Sessions: '(\d+)'(?:[\s\S]*)Max Header Length: '(\d+)'(?:[\s\S]*)Max URI Length: '(\d+)'(?:[\s\S]*)Max Stack Size: '(\d+)'"), timeout=15) + max_uri_handlers = int(result[0]) + max_sessions = int(result[1]) + max_hdr_len = int(result[2]) + max_uri_len = int(result[3]) + max_stack_size = int(result[4]) + + Utility.console_log("Got IP : " + got_ip) + Utility.console_log("Got Port : " + got_port) + + # Run test script + # If failed raise appropriate exception + failed = False + + Utility.console_log("Sessions and Context Tests...") + if not client.spillover_session(got_ip, got_port, max_sessions): + Utility.console_log("Ignoring failure") + if not client.parallel_sessions_adder(got_ip, got_port, max_sessions): + Utility.console_log("Ignoring failure") + if not client.leftover_data_test(got_ip, got_port): + failed = True + if not client.async_response_test(got_ip, got_port): + failed = True + if not client.recv_timeout_test(got_ip, got_port): + failed = True + + ## This test fails a lot! Enable when connection is stable + #test_size = 50*1024 # 50KB + #if not client.packet_size_limit_test(got_ip, got_port, test_size): + # Utility.console_log("Ignoring failure") + + Utility.console_log("Getting initial stack usage...") + if not client.get_hello(got_ip, got_port): + failed = True + + inital_stack = int(dut1.expect(re.compile(r"(?:[\s\S]*)Free Stack for server task: '(\d+)'"), timeout=15)[0]) + + if inital_stack < 0.1*max_stack_size: + Utility.console_log("More than 90% of stack being used on server start") + failed = True + + Utility.console_log("Basic HTTP Client Tests...") + if not client.get_hello(got_ip, got_port): + failed = True + if not client.post_hello(got_ip, got_port): + failed = True + if not client.put_hello(got_ip, got_port): + failed = True + if not client.post_echo(got_ip, got_port): + failed = True + if not client.get_echo(got_ip, got_port): + failed = True + if not client.put_echo(got_ip, got_port): + failed = True + if not client.get_hello_type(got_ip, got_port): + failed = True + if not client.get_hello_status(got_ip, got_port): + failed = True + if not client.get_false_uri(got_ip, got_port): + failed = True + + Utility.console_log("Error code tests...") + if not client.code_500_server_error_test(got_ip, got_port): + failed = True + if not client.code_501_method_not_impl(got_ip, got_port): + failed = True + if not client.code_505_version_not_supported(got_ip, got_port): + failed = True + if not client.code_400_bad_request(got_ip, got_port): + failed = True + if not client.code_404_not_found(got_ip, got_port): + failed = True + if not client.code_405_method_not_allowed(got_ip, got_port): + failed = True + if not client.code_408_req_timeout(got_ip, got_port): + failed = True + if not client.code_414_uri_too_long(got_ip, got_port, max_uri_len): + Utility.console_log("Ignoring failure") + if not client.code_431_hdr_too_long(got_ip, got_port, max_hdr_len): + Utility.console_log("Ignoring failure") + if not client.test_upgrade_not_supported(got_ip, got_port): + failed = True + + Utility.console_log("Getting final stack usage...") + if not client.get_hello(got_ip, got_port): + failed = True + + final_stack = int(dut1.expect(re.compile(r"(?:[\s\S]*)Free Stack for server task: '(\d+)'"), timeout=15)[0]) + + if final_stack < 0.05*max_stack_size: + Utility.console_log("More than 95% of stack got used during tests") + failed = True + + if failed: + raise RuntimeError + +if __name__ == '__main__': + test_examples_protocol_http_server_advanced() diff --git a/examples/protocols/http_server/advanced_tests/main/CMakeLists.txt b/examples/protocols/http_server/advanced_tests/main/CMakeLists.txt new file mode 100644 index 00000000..9fd69b1d --- /dev/null +++ b/examples/protocols/http_server/advanced_tests/main/CMakeLists.txt @@ -0,0 +1,5 @@ +set(COMPONENT_SRCS "main.c" + "tests.c") +set(COMPONENT_ADD_INCLUDEDIRS ". include") + +register_component() diff --git a/examples/protocols/http_server/advanced_tests/main/Kconfig.projbuild b/examples/protocols/http_server/advanced_tests/main/Kconfig.projbuild new file mode 100644 index 00000000..9e2813c6 --- /dev/null +++ b/examples/protocols/http_server/advanced_tests/main/Kconfig.projbuild @@ -0,0 +1,16 @@ +menu "Example Configuration" + +config WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + +config WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the example to use. + Can be left blank if the network has no security set. + +endmenu diff --git a/examples/protocols/http_server/advanced_tests/main/component.mk b/examples/protocols/http_server/advanced_tests/main/component.mk new file mode 100644 index 00000000..0b9d7585 --- /dev/null +++ b/examples/protocols/http_server/advanced_tests/main/component.mk @@ -0,0 +1,5 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + diff --git a/examples/protocols/http_server/advanced_tests/main/include/tests.h b/examples/protocols/http_server/advanced_tests/main/include/tests.h new file mode 100644 index 00000000..8f5db8ea --- /dev/null +++ b/examples/protocols/http_server/advanced_tests/main/include/tests.h @@ -0,0 +1,9 @@ +#ifndef __HTTPD_TESTS_H__ +#define __HTTPD_TESTS_H__ + +#include <esp_http_server.h> + +extern httpd_handle_t start_tests(void); +extern void stop_tests(httpd_handle_t hd); + +#endif // __HTTPD_TESTS_H__ diff --git a/examples/protocols/http_server/advanced_tests/main/main.c b/examples/protocols/http_server/advanced_tests/main/main.c new file mode 100644 index 00000000..de32966c --- /dev/null +++ b/examples/protocols/http_server/advanced_tests/main/main.c @@ -0,0 +1,81 @@ +#include "esp_wifi.h" +#include "esp_event_loop.h" +#include "esp_log.h" +#include "esp_system.h" +#include "nvs_flash.h" + +#include "tests.h" + +/* The examples use simple WiFi configuration that you can set via + 'make menuconfig'. + + If you'd rather not, just change the below entries to strings with + the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid" +*/ +#define EXAMPLE_WIFI_SSID CONFIG_WIFI_SSID +#define EXAMPLE_WIFI_PASS CONFIG_WIFI_PASSWORD + +static const char *TAG="TEST_WIFI"; + +static esp_err_t event_handler(void *ctx, system_event_t *event) +{ + httpd_handle_t *hd = (httpd_handle_t *) ctx; + + switch(event->event_id) { + case SYSTEM_EVENT_STA_START: + ESP_LOGI(TAG, "SYSTEM_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + break; + case SYSTEM_EVENT_STA_GOT_IP: + ESP_LOGI(TAG, "SYSTEM_EVENT_STA_GOT_IP"); + ESP_LOGI(TAG, "Got IP: '%s'", + ip4addr_ntoa(&event->event_info.got_ip.ip_info.ip)); + + // Start webserver tests + if (*hd == NULL) { + *hd = start_tests(); + } + + break; + case SYSTEM_EVENT_STA_DISCONNECTED: + ESP_LOGI(TAG, "SYSTEM_EVENT_STA_DISCONNECTED"); + ESP_ERROR_CHECK(esp_wifi_connect()); + + // Stop webserver tests + if (*hd) { + stop_tests(*hd); + *hd = NULL; + } + + break; + default: + break; + } + return ESP_OK; +} + +static void initialise_wifi(void) +{ + tcpip_adapter_init(); + static httpd_handle_t hd = NULL; + ESP_ERROR_CHECK(esp_event_loop_init(event_handler, &hd)); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + wifi_config_t wifi_config = { + .sta = { + .ssid = EXAMPLE_WIFI_SSID, + .password = EXAMPLE_WIFI_PASS, + }, + }; + ESP_LOGI(TAG, "Setting WiFi configuration SSID %s...", wifi_config.sta.ssid); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); +} + +void app_main() +{ + ESP_ERROR_CHECK(nvs_flash_init()); + initialise_wifi(); +} diff --git a/examples/protocols/http_server/advanced_tests/main/tests.c b/examples/protocols/http_server/advanced_tests/main/tests.c new file mode 100644 index 00000000..8845585b --- /dev/null +++ b/examples/protocols/http_server/advanced_tests/main/tests.c @@ -0,0 +1,310 @@ +#include <stdlib.h> +#include <stdbool.h> + +#include <esp_log.h> +#include <esp_system.h> +#include <esp_http_server.h> + +#include "tests.h" + +static const char *TAG="TESTS"; + +int pre_start_mem, post_stop_mem, post_stop_min_mem; +bool basic_sanity = true; + +struct async_resp_arg { + httpd_handle_t hd; + int fd; +}; + +/********************* Basic Handlers Start *******************/ + +esp_err_t hello_get_handler(httpd_req_t *req) +{ +#define STR "Hello World!" + ESP_LOGI(TAG, "Free Stack for server task: '%d'", uxTaskGetStackHighWaterMark(NULL)); + httpd_resp_send(req, STR, strlen(STR)); + return ESP_OK; +#undef STR +} + +esp_err_t hello_type_get_handler(httpd_req_t *req) +{ +#define STR "Hello World!" + httpd_resp_set_type(req, HTTPD_TYPE_TEXT); + httpd_resp_send(req, STR, strlen(STR)); + return ESP_OK; +#undef STR +} + +esp_err_t hello_status_get_handler(httpd_req_t *req) +{ +#define STR "Hello World!" + httpd_resp_set_status(req, HTTPD_500); + httpd_resp_send(req, STR, strlen(STR)); + return ESP_OK; +#undef STR +} + +esp_err_t echo_post_handler(httpd_req_t *req) +{ + ESP_LOGI(TAG, "/echo handler read content length %d", req->content_len); + + char* buf = malloc(req->content_len + 1); + size_t off = 0; + int ret; + + if (!buf) { + httpd_resp_send_500(req); + return ESP_FAIL; + } + + while (off < req->content_len) { + /* Read data received in the request */ + ret = httpd_req_recv(req, buf + off, req->content_len - off); + if (ret <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_408(req); + } + free (buf); + return ESP_FAIL; + } + off += ret; + ESP_LOGI(TAG, "/echo handler recv length %d", ret); + } + buf[off] = '\0'; + + if (req->content_len < 128) { + ESP_LOGI(TAG, "/echo handler read %s", buf); + } + + /* Search for Custom header field */ + char* req_hdr = 0; + size_t hdr_len = httpd_req_get_hdr_value_len(req, "Custom"); + if (hdr_len) { + /* Read Custom header value */ + req_hdr = malloc(hdr_len + 1); + if (req_hdr) { + httpd_req_get_hdr_value_str(req, "Custom", req_hdr, hdr_len + 1); + + /* Set as additional header for response packet */ + httpd_resp_set_hdr(req, "Custom", req_hdr); + } + } + httpd_resp_send(req, buf, req->content_len); + free (req_hdr); + free (buf); + return ESP_OK; +} + +void adder_free_func(void *ctx) +{ + ESP_LOGI(TAG, "Custom Free Context function called"); + free(ctx); +} + +/* Create a context, keep incrementing value in the context, by whatever was + * received. Return the result + */ +esp_err_t adder_post_handler(httpd_req_t *req) +{ + char buf[10]; + char outbuf[50]; + int ret; + + /* Read data received in the request */ + ret = httpd_req_recv(req, buf, sizeof(buf)); + if (ret <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_408(req); + } + return ESP_FAIL; + } + + buf[ret] = '\0'; + int val = atoi(buf); + ESP_LOGI(TAG, "/adder handler read %d", val); + + if (! req->sess_ctx) { + ESP_LOGI(TAG, "/adder allocating new session"); + req->sess_ctx = malloc(sizeof(int)); + req->free_ctx = adder_free_func; + *(int *)req->sess_ctx = 0; + } + int *adder = (int *)req->sess_ctx; + *adder += val; + + snprintf(outbuf, sizeof(outbuf),"%d", *adder); + httpd_resp_send(req, outbuf, strlen(outbuf)); + return ESP_OK; +} + +esp_err_t leftover_data_post_handler(httpd_req_t *req) +{ + /* Only echo the first 10 bytes of the request, leaving the rest of the + * request data as is. + */ + char buf[11]; + int ret; + + /* Read data received in the request */ + ret = httpd_req_recv(req, buf, sizeof(buf) - 1); + if (ret <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_408(req); + } + return ESP_FAIL; + } + + buf[ret] = '\0'; + ESP_LOGI(TAG, "leftover data handler read %s", buf); + httpd_resp_send(req, buf, strlen(buf)); + return ESP_OK; +} + +int httpd_default_send(httpd_handle_t hd, int sockfd, const char *buf, unsigned buf_len, int flags); +void generate_async_resp(void *arg) +{ + char buf[250]; + struct async_resp_arg *resp_arg = (struct async_resp_arg *)arg; + httpd_handle_t hd = resp_arg->hd; + int fd = resp_arg->fd; +#define HTTPD_HDR_STR "HTTP/1.1 200 OK\r\n" \ + "Content-Type: text/html\r\n" \ + "Content-Length: %d\r\n" +#define STR "Hello Double World!" + + ESP_LOGI(TAG, "Executing queued work fd : %d", fd); + + snprintf(buf, sizeof(buf), HTTPD_HDR_STR, + strlen(STR)); + httpd_default_send(hd, fd, buf, strlen(buf), 0); + /* Space for sending additional headers based on set_header */ + httpd_default_send(hd, fd, "\r\n", strlen("\r\n"), 0); + httpd_default_send(hd, fd, STR, strlen(STR), 0); +#undef STR + free(arg); +} + +esp_err_t async_get_handler(httpd_req_t *req) +{ +#define STR "Hello World!" + httpd_resp_send(req, STR, strlen(STR)); + /* Also register a HTTPD Work which sends the same data on the same + * socket again + */ + struct async_resp_arg *resp_arg = malloc(sizeof(struct async_resp_arg)); + resp_arg->hd = req->handle; + resp_arg->fd = httpd_req_to_sockfd(req); + if (resp_arg->fd < 0) { + return ESP_FAIL; + } + + ESP_LOGI(TAG, "Queuing work fd : %d", resp_arg->fd); + httpd_queue_work(req->handle, generate_async_resp, resp_arg); + return ESP_OK; +#undef STR +} + + +httpd_uri_t basic_handlers[] = { + { .uri = "/hello/type_html", + .method = HTTP_GET, + .handler = hello_type_get_handler, + .user_ctx = NULL, + }, + { .uri = "/hello", + .method = HTTP_GET, + .handler = hello_get_handler, + .user_ctx = NULL, + }, + { .uri = "/hello/status_500", + .method = HTTP_GET, + .handler = hello_status_get_handler, + .user_ctx = NULL, + }, + { .uri = "/echo", + .method = HTTP_POST, + .handler = echo_post_handler, + .user_ctx = NULL, + }, + { .uri = "/echo", + .method = HTTP_PUT, + .handler = echo_post_handler, + .user_ctx = NULL, + }, + { .uri = "/leftover_data", + .method = HTTP_POST, + .handler = leftover_data_post_handler, + .user_ctx = NULL, + }, + { .uri = "/adder", + .method = HTTP_POST, + .handler = adder_post_handler, + .user_ctx = NULL, + }, + { .uri = "/async_data", + .method = HTTP_GET, + .handler = async_get_handler, + .user_ctx = NULL, + } +}; + +int basic_handlers_no = sizeof(basic_handlers)/sizeof(httpd_uri_t); +void register_basic_handlers(httpd_handle_t hd) +{ + int i; + ESP_LOGI(TAG, "Registering basic handlers"); + ESP_LOGI(TAG, "No of handlers = %d", basic_handlers_no); + for (i = 0; i < basic_handlers_no; i++) { + if (httpd_register_uri_handler(hd, &basic_handlers[i]) != ESP_OK) { + ESP_LOGW(TAG, "register uri failed for %d", i); + return; + } + } + ESP_LOGI(TAG, "Success"); +} + +httpd_handle_t test_httpd_start() +{ + pre_start_mem = esp_get_free_heap_size(); + httpd_handle_t hd; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = 1234; + + /* This check should be a part of http_server */ + config.max_open_sockets = (CONFIG_LWIP_MAX_SOCKETS - 3); + + if (httpd_start(&hd, &config) == ESP_OK) { + ESP_LOGI(TAG, "Started HTTP server on port: '%d'", config.server_port); + ESP_LOGI(TAG, "Max URI handlers: '%d'", config.max_uri_handlers); + ESP_LOGI(TAG, "Max Open Sessions: '%d'", config.max_open_sockets); + ESP_LOGI(TAG, "Max Header Length: '%d'", HTTPD_MAX_REQ_HDR_LEN); + ESP_LOGI(TAG, "Max URI Length: '%d'", HTTPD_MAX_URI_LEN); + ESP_LOGI(TAG, "Max Stack Size: '%d'", config.stack_size); + return hd; + } + return NULL; +} + +void test_httpd_stop(httpd_handle_t hd) +{ + httpd_stop(hd); + post_stop_mem = esp_get_free_heap_size(); + ESP_LOGI(TAG, "HTTPD Stop: Current free memory: %d", post_stop_mem); +} + +httpd_handle_t start_tests() +{ + httpd_handle_t hd = test_httpd_start(); + if (hd) { + register_basic_handlers(hd); + } + return hd; +} + +void stop_tests(httpd_handle_t hd) +{ + ESP_LOGI(TAG, "Stopping httpd"); + test_httpd_stop(hd); +} diff --git a/examples/protocols/http_server/advanced_tests/scripts/test.py b/examples/protocols/http_server/advanced_tests/scripts/test.py new file mode 100644 index 00000000..4f8f0fd4 --- /dev/null +++ b/examples/protocols/http_server/advanced_tests/scripts/test.py @@ -0,0 +1,863 @@ +#!/usr/bin/env python +# +# Copyright 2018 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +# Utility for testing the web server. Test cases: +# Assume the device supports 'n' simultaneous open sockets +# +# HTTP Server Tests +# +# 0. Firmware Settings: +# - Create a dormant thread whose sole job is to call httpd_stop() when instructed +# - Measure the following before httpd_start() is called: +# - current free memory +# - current free sockets +# - Measure the same whenever httpd_stop is called +# - Register maximum possible URI handlers: should be successful +# - Register one more URI handler: should fail +# - Deregister on URI handler: should be successful +# - Register on more URI handler: should succeed +# - Register separate handlers for /hello, /hello/type_html. Also +# ensure that /hello/type_html is registered BEFORE /hello. (tests +# that largest matching URI is picked properly) +# - Create URI handler /adder. Make sure it uses a custom free_ctx +# structure to free it up + +# 1. Using Standard Python HTTP Client +# - simple GET on /hello (returns Hello World. Ensures that basic +# firmware tests are complete, or returns error) +# - POST on /hello (should fail) +# - PUT on /hello (should fail) +# - simple POST on /echo (returns whatever the POST data) +# - simple PUT on /echo (returns whatever the PUT data) +# - GET on /echo (should fail) +# - simple GET on /hello/type_html (returns Content type as text/html) +# - simple GET on /hello/status_500 (returns HTTP status 500) +# - simple GET on /false_uri (returns HTTP status 404) +# - largest matching URI handler is picked is already verified because +# of /hello and /hello/type_html tests +# +# +# 2. Session Tests +# - Sessions + Pipelining basics: +# - Create max supported sessions +# - On session i, +# - send 3 back-to-back POST requests with data i on /adder +# - read back 3 responses. They should be i, 2i and 3i +# - Tests that +# - pipelining works +# - per-session context is maintained for all supported +# sessions +# - Close all sessions +# +# - Cleanup leftover data: Tests that the web server properly cleans +# up leftover data +# - Create a session +# - POST on /leftover_data with 52 bytes of data (data includes +# \r\n)(the handler only +# reads first 10 bytes and returns them, leaving the rest of the +# bytes unread) +# - GET on /hello (should return 'Hello World') +# - POST on /false_uri with 52 bytes of data (data includes \r\n) +# (should return HTTP 404) +# - GET on /hello (should return 'Hello World') +# +# - Test HTTPd Asynchronous response +# - Create a session +# - GET on /async_data +# - returns 'Hello World!' as a response +# - the handler schedules an async response, which generates a second +# response 'Hello Double World!' +# +# - Spillover test +# - Create max supported sessions with the web server +# - GET /hello on all the sessions (should return Hello World) +# - Create one more session, this should fail +# - GET /hello on all the sessions (should return Hello World) +# +# - Timeout test +# - Create a session and only Send 'GE' on the same (simulates a +# client that left the network halfway through a request) +# - Wait for recv-wait-timeout +# - Server should automatically close the socket + + +############# TODO TESTS ############# + +# 3. Stress Tests +# +# - httperf +# - Run the following httperf command: +# httperf --server=10.31.130.126 --wsess=8,50,0.5 --rate 8 --burst-length 2 +# +# - The above implies that the test suite will open +# - 8 simultaneous connections with the server +# - the rate of opening the sessions will be 8 per sec. So in our +# case, a new connection will be opened every 0.2 seconds for 1 second +# - The burst length 2 indicates that 2 requests will be sent +# simultaneously on the same connection in a single go +# - 0.5 seconds is the time between sending out 2 bursts +# - 50 is the total number of requests that will be sent out +# +# - So in the above example, the test suite will open 8 +# connections, each separated by 0.2 seconds. On each connection +# it will send 2 requests in a single burst. The bursts on a +# single connection will be separated by 0.5 seconds. A total of +# 25 bursts (25 x 2 = 50) will be sent out + +# 4. Leak Tests +# - Simple Leak test +# - Simple GET on /hello/restart (returns success, stop web server, measures leaks, restarts webserver) +# - Simple GET on /hello/restart_results (returns the leak results) +# - Leak test with open sockets +# - Open 8 sessions +# - Simple GET on /hello/restart (returns success, stop web server, +# measures leaks, restarts webserver) +# - All sockets should get closed +# - Simple GET on /hello/restart_results (returns the leak results) + + +from __future__ import division +from __future__ import print_function +from future import standard_library +standard_library.install_aliases() +from builtins import str +from builtins import range +from builtins import object +import threading +import socket +import time +import argparse +import http.client +import sys +import string +import random +import Utility + +_verbose_ = False + +class Session(object): + def __init__(self, addr, port, timeout = 15): + self.client = socket.create_connection((addr, int(port)), timeout = timeout) + self.target = addr + self.status = 0 + self.encoding = '' + self.content_type = '' + self.content_len = 0 + + def send_err_check(self, request, data=None): + rval = True + try: + self.client.sendall(request.encode()); + if data: + self.client.sendall(data.encode()) + except socket.error as err: + self.client.close() + Utility.console_log("Socket Error in send :", err) + rval = False + return rval + + def send_get(self, path, headers=None): + request = "GET " + path + " HTTP/1.1\r\nHost: " + self.target + if headers: + for field, value in headers.items(): + request += "\r\n"+field+": "+value + request += "\r\n\r\n" + return self.send_err_check(request) + + def send_put(self, path, data, headers=None): + request = "PUT " + path + " HTTP/1.1\r\nHost: " + self.target + if headers: + for field, value in headers.items(): + request += "\r\n"+field+": "+value + request += "\r\nContent-Length: " + str(len(data)) +"\r\n\r\n" + return self.send_err_check(request, data) + + def send_post(self, path, data, headers=None): + request = "POST " + path + " HTTP/1.1\r\nHost: " + self.target + if headers: + for field, value in headers.items(): + request += "\r\n"+field+": "+value + request += "\r\nContent-Length: " + str(len(data)) +"\r\n\r\n" + return self.send_err_check(request, data) + + def read_resp_hdrs(self): + try: + state = 'nothing' + resp_read = '' + while True: + char = self.client.recv(1).decode() + if char == '\r' and state == 'nothing': + state = 'first_cr' + elif char == '\n' and state == 'first_cr': + state = 'first_lf' + elif char == '\r' and state == 'first_lf': + state = 'second_cr' + elif char == '\n' and state == 'second_cr': + state = 'second_lf' + else: + state = 'nothing' + resp_read += char + if state == 'second_lf': + break + # Handle first line + line_hdrs = resp_read.splitlines() + line_comp = line_hdrs[0].split() + self.status = line_comp[1] + del line_hdrs[0] + self.encoding = '' + self.content_type = '' + headers = dict() + # Process other headers + for h in range(len(line_hdrs)): + line_comp = line_hdrs[h].split(':') + if line_comp[0] == 'Content-Length': + self.content_len = int(line_comp[1]) + if line_comp[0] == 'Content-Type': + self.content_type = line_comp[1].lstrip() + if line_comp[0] == 'Transfer-Encoding': + self.encoding = line_comp[1].lstrip() + if len(line_comp) == 2: + headers[line_comp[0]] = line_comp[1].lstrip() + return headers + except socket.error as err: + self.client.close() + Utility.console_log("Socket Error in recv :", err) + return None + + def read_resp_data(self): + try: + read_data = '' + if self.encoding != 'chunked': + while len(read_data) != self.content_len: + read_data += self.client.recv(self.content_len).decode() + else: + chunk_data_buf = '' + while (True): + # Read one character into temp buffer + read_ch = self.client.recv(1) + # Check CRLF + if (read_ch == '\r'): + read_ch = self.client.recv(1).decode() + if (read_ch == '\n'): + # If CRLF decode length of chunk + chunk_len = int(chunk_data_buf, 16) + # Keep adding to contents + self.content_len += chunk_len + rem_len = chunk_len + while (rem_len): + new_data = self.client.recv(rem_len) + read_data += new_data + rem_len -= len(new_data) + chunk_data_buf = '' + # Fetch remaining CRLF + if self.client.recv(2) != "\r\n": + # Error in packet + Utility.console_log("Error in chunked data") + return None + if not chunk_len: + # If last chunk + break + continue + chunk_data_buf += '\r' + # If not CRLF continue appending + # character to chunked data buffer + chunk_data_buf += read_ch + return read_data + except socket.error as err: + self.client.close() + Utility.console_log("Socket Error in recv :", err) + return None + + def close(self): + self.client.close() + +def test_val(text, expected, received): + if expected != received: + Utility.console_log(" Fail!") + Utility.console_log(" [reason] " + text + ":") + Utility.console_log(" expected: " + str(expected)) + Utility.console_log(" received: " + str(received)) + return False + return True + +class adder_thread (threading.Thread): + def __init__(self, id, dut, port): + threading.Thread.__init__(self) + self.id = id + self.dut = dut + self.depth = 3 + self.session = Session(dut, port) + + def run(self): + self.response = [] + + # Pipeline 3 requests + if (_verbose_): + Utility.console_log(" Thread: Using adder start " + str(self.id)) + + for _ in range(self.depth): + self.session.send_post('/adder', str(self.id)) + time.sleep(2) + + for _ in range(self.depth): + self.session.read_resp_hdrs() + self.response.append(self.session.read_resp_data()) + + def adder_result(self): + if len(self.response) != self.depth: + Utility.console_log("Error : missing response packets") + return False + for i in range(len(self.response)): + if not test_val("Thread" + str(self.id) + " response[" + str(i) + "]", + str(self.id * (i + 1)), str(self.response[i])): + return False + return True + + def close(self): + self.session.close() + +def get_hello(dut, port): + # GET /hello should return 'Hello World!' + Utility.console_log("[test] GET /hello returns 'Hello World!' =>", end=' ') + conn = http.client.HTTPConnection(dut, int(port), timeout=15) + conn.request("GET", "/hello") + resp = conn.getresponse() + if not test_val("status_code", 200, resp.status): + conn.close() + return False + if not test_val("data", "Hello World!", resp.read().decode()): + conn.close() + return False + if not test_val("data", "text/html", resp.getheader('Content-Type')): + conn.close() + return False + Utility.console_log("Success") + conn.close() + return True + +def put_hello(dut, port): + # PUT /hello returns 405' + Utility.console_log("[test] PUT /hello returns 405 =>", end=' ') + conn = http.client.HTTPConnection(dut, int(port), timeout=15) + conn.request("PUT", "/hello", "Hello") + resp = conn.getresponse() + if not test_val("status_code", 405, resp.status): + conn.close() + return False + Utility.console_log("Success") + conn.close() + return True + +def post_hello(dut, port): + # POST /hello returns 405' + Utility.console_log("[test] POST /hello returns 404 =>", end=' ') + conn = http.client.HTTPConnection(dut, int(port), timeout=15) + conn.request("POST", "/hello", "Hello") + resp = conn.getresponse() + if not test_val("status_code", 405, resp.status): + conn.close() + return False + Utility.console_log("Success") + conn.close() + return True + +def post_echo(dut, port): + # POST /echo echoes data' + Utility.console_log("[test] POST /echo echoes data =>", end=' ') + conn = http.client.HTTPConnection(dut, int(port), timeout=15) + conn.request("POST", "/echo", "Hello") + resp = conn.getresponse() + if not test_val("status_code", 200, resp.status): + conn.close() + return False + if not test_val("data", "Hello", resp.read().decode()): + conn.close() + return False + Utility.console_log("Success") + conn.close() + return True + +def put_echo(dut, port): + # PUT /echo echoes data' + Utility.console_log("[test] PUT /echo echoes data =>", end=' ') + conn = http.client.HTTPConnection(dut, int(port), timeout=15) + conn.request("PUT", "/echo", "Hello") + resp = conn.getresponse() + if not test_val("status_code", 200, resp.status): + conn.close() + return False + if not test_val("data", "Hello", resp.read().decode()): + conn.close() + return False + Utility.console_log("Success") + conn.close() + return True + +def get_echo(dut, port): + # GET /echo returns 404' + Utility.console_log("[test] GET /echo returns 405 =>", end=' ') + conn = http.client.HTTPConnection(dut, int(port), timeout=15) + conn.request("GET", "/echo") + resp = conn.getresponse() + if not test_val("status_code", 405, resp.status): + conn.close() + return False + Utility.console_log("Success") + conn.close() + return True + +def get_hello_type(dut, port): + # GET /hello/type_html returns text/html as Content-Type' + Utility.console_log("[test] GET /hello/type_html has Content-Type of text/html =>", end=' ') + conn = http.client.HTTPConnection(dut, int(port), timeout=15) + conn.request("GET", "/hello/type_html") + resp = conn.getresponse() + if not test_val("status_code", 200, resp.status): + conn.close() + return False + if not test_val("data", "Hello World!", resp.read().decode()): + conn.close() + return False + if not test_val("data", "text/html", resp.getheader('Content-Type')): + conn.close() + return False + Utility.console_log("Success") + conn.close() + return True + +def get_hello_status(dut, port): + # GET /hello/status_500 returns status 500' + Utility.console_log("[test] GET /hello/status_500 returns status 500 =>", end=' ') + conn = http.client.HTTPConnection(dut, int(port), timeout=15) + conn.request("GET", "/hello/status_500") + resp = conn.getresponse() + if not test_val("status_code", 500, resp.status): + conn.close() + return False + Utility.console_log("Success") + conn.close() + return True + +def get_false_uri(dut, port): + # GET /false_uri returns status 404' + Utility.console_log("[test] GET /false_uri returns status 404 =>", end=' ') + conn = http.client.HTTPConnection(dut, int(port), timeout=15) + conn.request("GET", "/false_uri") + resp = conn.getresponse() + if not test_val("status_code", 404, resp.status): + conn.close() + return False + Utility.console_log("Success") + conn.close() + return True + +def parallel_sessions_adder(dut, port, max_sessions): + # POSTs on /adder in parallel sessions + Utility.console_log("[test] POST {pipelined} on /adder in " + str(max_sessions) + " sessions =>", end=' ') + t = [] + # Create all sessions + for i in range(max_sessions): + t.append(adder_thread(i, dut, port)) + + for i in range(len(t)): + t[i].start() + + for i in range(len(t)): + t[i].join() + + res = True + for i in range(len(t)): + if not test_val("Thread" + str(i) + " Failed", t[i].adder_result(), True): + res = False + t[i].close() + if (res): + Utility.console_log("Success") + return res + +def async_response_test(dut, port): + # Test that an asynchronous work is executed in the HTTPD's context + # This is tested by reading two responses over the same session + Utility.console_log("[test] Test HTTPD Work Queue (Async response) =>", end=' ') + s = Session(dut, port) + + s.send_get('/async_data') + s.read_resp_hdrs() + if not test_val("First Response", "Hello World!", s.read_resp_data()): + s.close() + return False + s.read_resp_hdrs() + if not test_val("Second Response", "Hello Double World!", s.read_resp_data()): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +def leftover_data_test(dut, port): + # Leftover data in POST is purged (valid and invalid URIs) + Utility.console_log("[test] Leftover data in POST is purged (valid and invalid URIs) =>", end=' ') + s = http.client.HTTPConnection(dut + ":" + port, timeout=15) + + s.request("POST", url='/leftover_data', body="abcdefghijklmnopqrstuvwxyz\r\nabcdefghijklmnopqrstuvwxyz") + resp = s.getresponse() + if not test_val("Partial data", "abcdefghij", resp.read().decode()): + s.close() + return False + + s.request("GET", url='/hello') + resp = s.getresponse() + if not test_val("Hello World Data", "Hello World!", resp.read().decode()): + s.close() + return False + + s.request("POST", url='/false_uri', body="abcdefghijklmnopqrstuvwxyz\r\nabcdefghijklmnopqrstuvwxyz") + resp = s.getresponse() + if not test_val("False URI Status", str(404), str(resp.status)): + s.close() + return False + resp.read() + + s.request("GET", url='/hello') + resp = s.getresponse() + if not test_val("Hello World Data", "Hello World!", resp.read().decode()): + s.close() + return False + + s.close() + Utility.console_log("Success") + return True + +def spillover_session(dut, port, max_sess): + # Session max_sess_sessions + 1 is rejected + Utility.console_log("[test] Session max_sess_sessions (" + str(max_sess) + ") + 1 is rejected =>", end=' ') + s = [] + _verbose_ = True + for i in range(max_sess + 1): + if (_verbose_): + Utility.console_log("Executing " + str(i)) + try: + a = http.client.HTTPConnection(dut + ":" + port, timeout=15) + a.request("GET", url='/hello') + resp = a.getresponse() + if not test_val("Connection " + str(i), "Hello World!", resp.read().decode()): + a.close() + break + s.append(a) + except: + if (_verbose_): + Utility.console_log("Connection " + str(i) + " rejected") + a.close() + break + + # Close open connections + for a in s: + a.close() + + # Check if number of connections is equal to max_sess + Utility.console_log(["Fail","Success"][len(s) == max_sess]) + return (len(s) == max_sess) + +def recv_timeout_test(dut, port): + Utility.console_log("[test] Timeout occurs if partial packet sent =>", end=' ') + s = Session(dut, port) + s.client.sendall(b"GE") + s.read_resp_hdrs() + resp = s.read_resp_data() + if not test_val("Request Timeout", "Server closed this connection", resp): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +def packet_size_limit_test(dut, port, test_size): + Utility.console_log("[test] send size limit test =>", end=' ') + retry = 5 + while (retry): + retry -= 1 + Utility.console_log("data size = ", test_size) + s = http.client.HTTPConnection(dut + ":" + port, timeout=15) + random_data = ''.join(string.printable[random.randint(0,len(string.printable))-1] for _ in list(range(test_size))) + path = "/echo" + s.request("POST", url=path, body=random_data) + resp = s.getresponse() + if not test_val("Error", "200", str(resp.status)): + if test_val("Error", "500", str(resp.status)): + Utility.console_log("Data too large to be allocated") + test_size = test_size//10 + else: + Utility.console_log("Unexpected error") + s.close() + Utility.console_log("Retry...") + continue + resp = resp.read().decode() + result = (resp == random_data) + if not result: + test_val("Data size", str(len(random_data)), str(len(resp))) + s.close() + Utility.console_log("Retry...") + continue + s.close() + Utility.console_log("Success") + return True + Utility.console_log("Failed") + return False + +def code_500_server_error_test(dut, port): + Utility.console_log("[test] 500 Server Error test =>", end=' ') + s = Session(dut, port) + # Sending a very large content length will cause malloc to fail + content_len = 2**31 + s.client.sendall(("POST /echo HTTP/1.1\r\nHost: " + dut + "\r\nContent-Length: " + str(content_len) + "\r\n\r\nABCD").encode()) + s.read_resp_hdrs() + resp = s.read_resp_data() + if not test_val("Server Error", "500", s.status): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +def code_501_method_not_impl(dut, port): + Utility.console_log("[test] 501 Method Not Implemented =>", end=' ') + s = Session(dut, port) + path = "/hello" + s.client.sendall(("ABC " + path + " HTTP/1.1\r\nHost: " + dut + "\r\n\r\n").encode()) + s.read_resp_hdrs() + resp = s.read_resp_data() + # Presently server sends back 400 Bad Request + #if not test_val("Server Error", "501", s.status): + #s.close() + #return False + if not test_val("Server Error", "400", s.status): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +def code_505_version_not_supported(dut, port): + Utility.console_log("[test] 505 Version Not Supported =>", end=' ') + s = Session(dut, port) + path = "/hello" + s.client.sendall(("GET " + path + " HTTP/2.0\r\nHost: " + dut + "\r\n\r\n").encode()) + s.read_resp_hdrs() + resp = s.read_resp_data() + if not test_val("Server Error", "505", s.status): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +def code_400_bad_request(dut, port): + Utility.console_log("[test] 400 Bad Request =>", end=' ') + s = Session(dut, port) + path = "/hello" + s.client.sendall(("XYZ " + path + " HTTP/1.1\r\nHost: " + dut + "\r\n\r\n").encode()) + s.read_resp_hdrs() + resp = s.read_resp_data() + if not test_val("Client Error", "400", s.status): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +def code_404_not_found(dut, port): + Utility.console_log("[test] 404 Not Found =>", end=' ') + s = Session(dut, port) + path = "/dummy" + s.client.sendall(("GET " + path + " HTTP/1.1\r\nHost: " + dut + "\r\n\r\n").encode()) + s.read_resp_hdrs() + resp = s.read_resp_data() + if not test_val("Client Error", "404", s.status): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +def code_405_method_not_allowed(dut, port): + Utility.console_log("[test] 405 Method Not Allowed =>", end=' ') + s = Session(dut, port) + path = "/hello" + s.client.sendall(("POST " + path + " HTTP/1.1\r\nHost: " + dut + "\r\n\r\n").encode()) + s.read_resp_hdrs() + resp = s.read_resp_data() + if not test_val("Client Error", "405", s.status): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +def code_408_req_timeout(dut, port): + Utility.console_log("[test] 408 Request Timeout =>", end=' ') + s = Session(dut, port) + s.client.sendall(("POST /echo HTTP/1.1\r\nHost: " + dut + "\r\nContent-Length: 10\r\n\r\nABCD").encode()) + s.read_resp_hdrs() + resp = s.read_resp_data() + if not test_val("Client Error", "408", s.status): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +def code_411_length_required(dut, port): + Utility.console_log("[test] 411 Length Required =>", end=' ') + s = Session(dut, port) + path = "/echo" + s.client.sendall(("POST " + path + " HTTP/1.1\r\nHost: " + dut + "\r\nContent-Type: text/plain\r\nTransfer-Encoding: chunked\r\n\r\n").encode()) + s.read_resp_hdrs() + resp = s.read_resp_data() + # Presently server sends back 400 Bad Request + #if not test_val("Client Error", "411", s.status): + #s.close() + #return False + if not test_val("Client Error", "400", s.status): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +def send_getx_uri_len(dut, port, length): + s = Session(dut, port) + method = "GET " + version = " HTTP/1.1\r\n" + path = "/"+"x"*(length - len(method) - len(version) - len("/")) + s.client.sendall(method.encode()) + time.sleep(1) + s.client.sendall(path.encode()) + time.sleep(1) + s.client.sendall((version + "Host: " + dut + "\r\n\r\n").encode()) + s.read_resp_hdrs() + resp = s.read_resp_data() + s.close() + return s.status + +def code_414_uri_too_long(dut, port, max_uri_len): + Utility.console_log("[test] 414 URI Too Long =>", end=' ') + status = send_getx_uri_len(dut, port, max_uri_len) + if not test_val("Client Error", "404", status): + return False + status = send_getx_uri_len(dut, port, max_uri_len + 1) + if not test_val("Client Error", "414", status): + return False + Utility.console_log("Success") + return True + +def send_postx_hdr_len(dut, port, length): + s = Session(dut, port) + path = "/echo" + host = "Host: " + dut + custom_hdr_field = "\r\nCustom: " + custom_hdr_val = "x"*(length - len(host) - len(custom_hdr_field) - len("\r\n\r\n") + len("0")) + request = ("POST " + path + " HTTP/1.1\r\n" + host + custom_hdr_field + custom_hdr_val + "\r\n\r\n").encode() + s.client.sendall(request[:length//2]) + time.sleep(1) + s.client.sendall(request[length//2:]) + hdr = s.read_resp_hdrs() + resp = s.read_resp_data() + s.close() + if "Custom" in hdr: + return (hdr["Custom"] == custom_hdr_val), resp + return False, s.status + +def code_431_hdr_too_long(dut, port, max_hdr_len): + Utility.console_log("[test] 431 Header Too Long =>", end=' ') + res, status = send_postx_hdr_len(dut, port, max_hdr_len) + if not res: + return False + res, status = send_postx_hdr_len(dut, port, max_hdr_len + 1) + if not test_val("Client Error", "431", status): + return False + Utility.console_log("Success") + return True + +def test_upgrade_not_supported(dut, port): + Utility.console_log("[test] Upgrade Not Supported =>", end=' ') + s = Session(dut, port) + path = "/hello" + s.client.sendall(("OPTIONS * HTTP/1.1\r\nHost:" + dut + "\r\nUpgrade: TLS/1.0\r\nConnection: Upgrade\r\n\r\n").encode()) + s.read_resp_hdrs() + resp = s.read_resp_data() + if not test_val("Client Error", "200", s.status): + s.close() + return False + s.close() + Utility.console_log("Success") + return True + +if __name__ == '__main__': + ########### Execution begins here... + # Configuration + # Max number of threads/sessions + max_sessions = 7 + max_uri_len = 512 + max_hdr_len = 512 + + parser = argparse.ArgumentParser(description='Run HTTPD Test') + parser.add_argument('-4','--ipv4', help='IPv4 address') + parser.add_argument('-6','--ipv6', help='IPv6 address') + parser.add_argument('-p','--port', help='Port') + args = vars(parser.parse_args()) + + dut4 = args['ipv4'] + dut6 = args['ipv6'] + port = args['port'] + dut = dut4 + + _verbose_ = True + + Utility.console_log("### Basic HTTP Client Tests") + get_hello(dut, port) + post_hello(dut, port) + put_hello(dut, port) + post_echo(dut, port) + get_echo(dut, port) + put_echo(dut, port) + get_hello_type(dut, port) + get_hello_status(dut, port) + get_false_uri(dut, port) + + Utility.console_log("### Error code tests") + code_500_server_error_test(dut, port) + code_501_method_not_impl(dut, port) + code_505_version_not_supported(dut, port) + code_400_bad_request(dut, port) + code_404_not_found(dut, port) + code_405_method_not_allowed(dut, port) + code_408_req_timeout(dut, port) + code_414_uri_too_long(dut, port, max_uri_len) + code_431_hdr_too_long(dut, port, max_hdr_len) + test_upgrade_not_supported(dut, port) + + # Not supported yet (Error on chunked request) + ###code_411_length_required(dut, port) + + Utility.console_log("### Sessions and Context Tests") + parallel_sessions_adder(dut, port, max_sessions) + leftover_data_test(dut, port) + async_response_test(dut, port) + spillover_session(dut, port, max_sessions) + recv_timeout_test(dut, port) + packet_size_limit_test(dut, port, 50*1024) + get_hello(dut, port) + + sys.exit() diff --git a/examples/protocols/http_server/persistent_sockets/CMakeLists.txt b/examples/protocols/http_server/persistent_sockets/CMakeLists.txt new file mode 100644 index 00000000..2d453b67 --- /dev/null +++ b/examples/protocols/http_server/persistent_sockets/CMakeLists.txt @@ -0,0 +1,7 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(persistent_sockets) + diff --git a/examples/protocols/http_server/persistent_sockets/Makefile b/examples/protocols/http_server/persistent_sockets/Makefile new file mode 100644 index 00000000..9c178022 --- /dev/null +++ b/examples/protocols/http_server/persistent_sockets/Makefile @@ -0,0 +1,9 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := persistent_sockets + +include $(IDF_PATH)/make/project.mk + diff --git a/examples/protocols/http_server/persistent_sockets/README.md b/examples/protocols/http_server/persistent_sockets/README.md new file mode 100644 index 00000000..b7517d8f --- /dev/null +++ b/examples/protocols/http_server/persistent_sockets/README.md @@ -0,0 +1,18 @@ +# HTTPD Server Persistant Sockets Example + +The Example consists of HTTPD server persistent sockets demo. +This sort of persistancy enables the server to have independent sessions/contexts per client. + +* Configure the project using "make menuconfig" and goto : + * Example Configuration -> + 1. WIFI SSID: WIFI network to which your PC is also connected to. + 2. WIFI Password: WIFI password + +* In order to test the HTTPD server persistent sockets demo : + 1. compile and burn the firmware "make flash" + 2. run "make monitor" and note down the IP assigned to your ESP module. The default port is 80 + 3. run the test script "python2 scripts/adder.py \<IP\> \<port\> \<N\>" + * the provided test script sends (POST) numbers from 1 to N to the server which has a URI POST handler for adding these numbers into an accumulator that is valid throughout the lifetime of the connection socket, hence persistent + * the script does a GET before closing and displays the final value of the accumulator + +See the README.md file in the upper level 'examples' directory for more information about examples. diff --git a/examples/protocols/http_server/persistent_sockets/http_server_persistence_test.py b/examples/protocols/http_server/persistent_sockets/http_server_persistence_test.py new file mode 100644 index 00000000..79f70624 --- /dev/null +++ b/examples/protocols/http_server/persistent_sockets/http_server_persistence_test.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python +# +# Copyright 2018 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import str +from builtins import range +import imp +import re +import os +import sys +import string +import random +import socket + +# This environment variable is expected on the host machine +test_fw_path = os.getenv("TEST_FW_PATH") +if test_fw_path and test_fw_path not in sys.path: + sys.path.insert(0, test_fw_path) + +# When running on local machine execute the following before running this script +# > make app bootloader +# > make print_flash_cmd | tail -n 1 > build/download.config +# > export TEST_FW_PATH=~/esp/esp-idf/tools/tiny-test-fw + +import TinyFW +import IDF +import Utility + +# Import client module +expath = os.path.dirname(os.path.realpath(__file__)) +client = imp.load_source("client", expath + "/scripts/adder.py") + +@IDF.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_http_server_persistence(env, extra_data): + # Acquire DUT + dut1 = env.get_dut("http_server", "examples/protocols/http_server/persistent_sockets") + + # Get binary file + binary_file = os.path.join(dut1.app.binary_path, "persistent_sockets.bin") + bin_size = os.path.getsize(binary_file) + IDF.log_performance("http_server_bin_size", "{}KB".format(bin_size//1024)) + IDF.check_performance("http_server_bin_size", bin_size//1024) + + # Upload binary and start testing + Utility.console_log("Starting http_server persistance test app") + dut1.start_app() + + # Parse IP address of STA + Utility.console_log("Waiting to connect with AP") + got_ip = dut1.expect(re.compile(r"(?:[\s\S]*)Got IP: '(\d+.\d+.\d+.\d+)'"), timeout=120)[0] + got_port = dut1.expect(re.compile(r"(?:[\s\S]*)Starting server on port: '(\d+)'"), timeout=30)[0] + + Utility.console_log("Got IP : " + got_ip) + Utility.console_log("Got Port : " + got_port) + + # Expected Logs + dut1.expect("Registering URI handlers", timeout=30) + + # Run test script + conn = client.start_session(got_ip, got_port) + visitor = 0 + adder = 0 + + # Test PUT request and initialize session context + num = random.randint(0,100) + client.putreq(conn, "/adder", str(num)) + visitor += 1 + dut1.expect("/adder visitor count = " + str(visitor), timeout=30) + dut1.expect("/adder PUT handler read " + str(num), timeout=30) + dut1.expect("PUT allocating new session", timeout=30) + + # Retest PUT request and change session context value + num = random.randint(0,100) + Utility.console_log("Adding: " + str(num)) + client.putreq(conn, "/adder", str(num)) + visitor += 1 + adder += num + dut1.expect("/adder visitor count = " + str(visitor), timeout=30) + dut1.expect("/adder PUT handler read " + str(num), timeout=30) + try: + # Re allocation shouldn't happen + dut1.expect("PUT allocating new session", timeout=30) + # Not expected + raise RuntimeError + except: + # As expected + pass + + # Test POST request and session persistence + random_nums = [random.randint(0,100) for _ in range(100)] + for num in random_nums: + Utility.console_log("Adding: " + str(num)) + client.postreq(conn, "/adder", str(num)) + visitor += 1 + adder += num + dut1.expect("/adder visitor count = " + str(visitor), timeout=30) + dut1.expect("/adder handler read " + str(num), timeout=30) + + # Test GET request and session persistence + Utility.console_log("Matching final sum: " + str(adder)) + if client.getreq(conn, "/adder").decode() != str(adder): + raise RuntimeError + visitor += 1 + dut1.expect("/adder visitor count = " + str(visitor), timeout=30) + dut1.expect("/adder GET handler send " + str(adder), timeout=30) + + Utility.console_log("Ending session") + # Close connection and check for invocation of context "Free" function + client.end_session(conn) + dut1.expect("/adder Free Context function called", timeout=30) + + Utility.console_log("Validating user context data") + # Start another session to check user context data + conn2 = client.start_session(got_ip, got_port) + num = random.randint(0,100) + client.putreq(conn, "/adder", str(num)) + visitor += 1 + dut1.expect("/adder visitor count = " + str(visitor), timeout=30) + dut1.expect("/adder PUT handler read " + str(num), timeout=30) + dut1.expect("PUT allocating new session", timeout=30) + client.end_session(conn) + dut1.expect("/adder Free Context function called", timeout=30) + +if __name__ == '__main__': + test_examples_protocol_http_server_persistence() diff --git a/examples/protocols/http_server/persistent_sockets/main/CMakeLists.txt b/examples/protocols/http_server/persistent_sockets/main/CMakeLists.txt new file mode 100644 index 00000000..85970762 --- /dev/null +++ b/examples/protocols/http_server/persistent_sockets/main/CMakeLists.txt @@ -0,0 +1,4 @@ +set(COMPONENT_SRCS "main.c") +set(COMPONENT_ADD_INCLUDEDIRS ".") + +register_component() diff --git a/examples/protocols/http_server/persistent_sockets/main/Kconfig.projbuild b/examples/protocols/http_server/persistent_sockets/main/Kconfig.projbuild new file mode 100644 index 00000000..9e2813c6 --- /dev/null +++ b/examples/protocols/http_server/persistent_sockets/main/Kconfig.projbuild @@ -0,0 +1,16 @@ +menu "Example Configuration" + +config WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + +config WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the example to use. + Can be left blank if the network has no security set. + +endmenu diff --git a/examples/protocols/http_server/persistent_sockets/main/component.mk b/examples/protocols/http_server/persistent_sockets/main/component.mk new file mode 100644 index 00000000..0b9d7585 --- /dev/null +++ b/examples/protocols/http_server/persistent_sockets/main/component.mk @@ -0,0 +1,5 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + diff --git a/examples/protocols/http_server/persistent_sockets/main/main.c b/examples/protocols/http_server/persistent_sockets/main/main.c new file mode 100644 index 00000000..b3535592 --- /dev/null +++ b/examples/protocols/http_server/persistent_sockets/main/main.c @@ -0,0 +1,252 @@ +/* Persistent Sockets Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include <esp_wifi.h> +#include <esp_event_loop.h> +#include <esp_log.h> +#include <esp_system.h> +#include <nvs_flash.h> + +#include <esp_http_server.h> + +/* An example to demonstrate persistent sockets, with context maintained across + * multiple requests on that socket. + * The examples use simple WiFi configuration that you can set via 'make menuconfig'. + * If you'd rather not, just change the below entries to strings with + * the config you want - ie #define EXAMPLE_WIFI_SSID "mywifissid" + */ +#define EXAMPLE_WIFI_SSID CONFIG_WIFI_SSID +#define EXAMPLE_WIFI_PASS CONFIG_WIFI_PASSWORD + +static const char *TAG="APP"; + +/* Function to free context */ +void adder_free_func(void *ctx) +{ + ESP_LOGI(TAG, "/adder Free Context function called"); + free(ctx); +} + +/* This handler keeps accumulating data that is posted to it into a per + * socket/session context. And returns the result. + */ +esp_err_t adder_post_handler(httpd_req_t *req) +{ + /* Log total visitors */ + unsigned *visitors = (unsigned *)req->user_ctx; + ESP_LOGI(TAG, "/adder visitor count = %d", ++(*visitors)); + + char buf[10]; + char outbuf[50]; + int ret; + + /* Read data received in the request */ + ret = httpd_req_recv(req, buf, sizeof(buf)); + if (ret <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_408(req); + } + return ESP_FAIL; + } + + buf[ret] = '\0'; + int val = atoi(buf); + ESP_LOGI(TAG, "/adder handler read %d", val); + + /* Create session's context if not already available */ + if (! req->sess_ctx) { + ESP_LOGI(TAG, "/adder allocating new session"); + req->sess_ctx = malloc(sizeof(int)); + req->free_ctx = adder_free_func; + *(int *)req->sess_ctx = 0; + } + + /* Add the received data to the context */ + int *adder = (int *)req->sess_ctx; + *adder += val; + + /* Respond with the accumulated value */ + snprintf(outbuf, sizeof(outbuf),"%d", *adder); + httpd_resp_send(req, outbuf, strlen(outbuf)); + return ESP_OK; +} + +/* This handler gets the present value of the accumulator */ +esp_err_t adder_get_handler(httpd_req_t *req) +{ + /* Log total visitors */ + unsigned *visitors = (unsigned *)req->user_ctx; + ESP_LOGI(TAG, "/adder visitor count = %d", ++(*visitors)); + + char outbuf[50]; + + /* Create session's context if not already available */ + if (! req->sess_ctx) { + ESP_LOGI(TAG, "/adder GET allocating new session"); + req->sess_ctx = malloc(sizeof(int)); + req->free_ctx = adder_free_func; + *(int *)req->sess_ctx = 0; + } + ESP_LOGI(TAG, "/adder GET handler send %d", *(int *)req->sess_ctx); + + /* Respond with the accumulated value */ + snprintf(outbuf, sizeof(outbuf),"%d", *((int *)req->sess_ctx)); + httpd_resp_send(req, outbuf, strlen(outbuf)); + return ESP_OK; +} + +/* This handler resets the value of the accumulator */ +esp_err_t adder_put_handler(httpd_req_t *req) +{ + /* Log total visitors */ + unsigned *visitors = (unsigned *)req->user_ctx; + ESP_LOGI(TAG, "/adder visitor count = %d", ++(*visitors)); + + char buf[10]; + char outbuf[50]; + int ret; + + /* Read data received in the request */ + ret = httpd_req_recv(req, buf, sizeof(buf)); + if (ret <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_408(req); + } + return ESP_FAIL; + } + + buf[ret] = '\0'; + int val = atoi(buf); + ESP_LOGI(TAG, "/adder PUT handler read %d", val); + + /* Create session's context if not already available */ + if (! req->sess_ctx) { + ESP_LOGI(TAG, "/adder PUT allocating new session"); + req->sess_ctx = malloc(sizeof(int)); + req->free_ctx = adder_free_func; + } + *(int *)req->sess_ctx = val; + + /* Respond with the reset value */ + snprintf(outbuf, sizeof(outbuf),"%d", *((int *)req->sess_ctx)); + httpd_resp_send(req, outbuf, strlen(outbuf)); + return ESP_OK; +} + +/* Maintain a variable which stores the number of times + * the "/adder" URI has been visited */ +static unsigned visitors = 0; + +httpd_uri_t adder_post = { + .uri = "/adder", + .method = HTTP_POST, + .handler = adder_post_handler, + .user_ctx = &visitors +}; + +httpd_uri_t adder_get = { + .uri = "/adder", + .method = HTTP_GET, + .handler = adder_get_handler, + .user_ctx = &visitors +}; + +httpd_uri_t adder_put = { + .uri = "/adder", + .method = HTTP_PUT, + .handler = adder_put_handler, + .user_ctx = &visitors +}; + +httpd_handle_t start_webserver(void) +{ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + // Start the httpd server + ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); + httpd_handle_t server; + + if (httpd_start(&server, &config) == ESP_OK) { + // Set URI handlers + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(server, &adder_get); + httpd_register_uri_handler(server, &adder_put); + httpd_register_uri_handler(server, &adder_post); + return server; + } + + ESP_LOGI(TAG, "Error starting server!"); + return NULL; +} + +void stop_webserver(httpd_handle_t server) +{ + // Stop the httpd server + httpd_stop(server); +} + +static esp_err_t event_handler(void *ctx, system_event_t *event) +{ + httpd_handle_t *server = (httpd_handle_t *) ctx; + + switch(event->event_id) { + case SYSTEM_EVENT_STA_START: + ESP_LOGI(TAG, "SYSTEM_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + break; + case SYSTEM_EVENT_STA_GOT_IP: + ESP_LOGI(TAG, "SYSTEM_EVENT_STA_GOT_IP"); + ESP_LOGI(TAG, "Got IP: '%s'", + ip4addr_ntoa(&event->event_info.got_ip.ip_info.ip)); + + /* Start the web server */ + if (*server == NULL) { + *server = start_webserver(); + } + break; + case SYSTEM_EVENT_STA_DISCONNECTED: + ESP_LOGI(TAG, "SYSTEM_EVENT_STA_DISCONNECTED"); + ESP_ERROR_CHECK(esp_wifi_connect()); + + /* Stop the webserver */ + if (*server) { + stop_webserver(*server); + *server = NULL; + } + break; + default: + break; + } + return ESP_OK; +} + +static void initialise_wifi(void *arg) +{ + tcpip_adapter_init(); + ESP_ERROR_CHECK(esp_event_loop_init(event_handler, arg)); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + wifi_config_t wifi_config = { + .sta = { + .ssid = EXAMPLE_WIFI_SSID, + .password = EXAMPLE_WIFI_PASS, + }, + }; + ESP_LOGI(TAG, "Setting WiFi configuration SSID %s...", wifi_config.sta.ssid); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); +} + +void app_main() +{ + static httpd_handle_t server = NULL; + ESP_ERROR_CHECK(nvs_flash_init()); + initialise_wifi(&server); +} diff --git a/examples/protocols/http_server/persistent_sockets/scripts/adder.py b/examples/protocols/http_server/persistent_sockets/scripts/adder.py new file mode 100644 index 00000000..55ca7508 --- /dev/null +++ b/examples/protocols/http_server/persistent_sockets/scripts/adder.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# +# Copyright 2018 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +from __future__ import print_function +from __future__ import unicode_literals +from future import standard_library +standard_library.install_aliases() +from builtins import str +from builtins import range +import http.client +import argparse + +def start_session (ip, port): + return http.client.HTTPConnection(ip, int(port), timeout=15) + +def end_session (conn): + conn.close() + +def getreq (conn, path, verbose = False): + conn.request("GET", path) + resp = conn.getresponse() + data = resp.read() + if verbose: + Utility.console_log("GET : " + path) + Utility.console_log("Status : " + resp.status) + Utility.console_log("Reason : " + resp.reason) + Utility.console_log("Data length : " + str(len(data))) + Utility.console_log("Data content : " + data) + return data + +def postreq (conn, path, data, verbose = False): + conn.request("POST", path, data) + resp = conn.getresponse() + data = resp.read() + if verbose: + Utility.console_log("POST : " + data) + Utility.console_log("Status : " + resp.status) + Utility.console_log("Reason : " + resp.reason) + Utility.console_log("Data length : " + str(len(data))) + Utility.console_log("Data content : " + data) + return data + +def putreq (conn, path, body, verbose = False): + conn.request("PUT", path, body) + resp = conn.getresponse() + data = resp.read() + if verbose: + Utility.console_log("PUT : " + path, body) + Utility.console_log("Status : " + resp.status) + Utility.console_log("Reason : " + resp.reason) + Utility.console_log("Data length : " + str(len(data))) + Utility.console_log("Data content : " + data) + return data + +if __name__ == '__main__': + # Configure argument parser + parser = argparse.ArgumentParser(description='Run HTTPd Test') + parser.add_argument('IP' , metavar='IP' , type=str, help='Server IP') + parser.add_argument('port', metavar='port', type=str, help='Server port') + parser.add_argument('N' , metavar='integer', type=int, help='Integer to sum upto') + args = vars(parser.parse_args()) + + # Get arguments + ip = args['IP'] + port = args['port'] + N = args['N'] + + # Establish HTTP connection + Utility.console_log("Connecting to => " + ip + ":" + port) + conn = start_session (ip, port) + + # Reset adder context to specified value(0) + # -- Not needed as new connection will always + # -- have zero value of the accumulator + Utility.console_log("Reset the accumulator to 0") + putreq (conn, "/adder", str(0)) + + # Sum numbers from 1 to specified value(N) + Utility.console_log("Summing numbers from 1 to " + str(N)) + for i in range(1, N+1): + postreq (conn, "/adder", str(i)) + + # Fetch the result + Utility.console_log("Result :" + getreq (conn, "/adder")) + + # Close HTTP connection + end_session (conn) diff --git a/examples/protocols/http_server/simple/CMakeLists.txt b/examples/protocols/http_server/simple/CMakeLists.txt new file mode 100644 index 00000000..cc9d4fd8 --- /dev/null +++ b/examples/protocols/http_server/simple/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(simple) diff --git a/examples/protocols/http_server/simple/Makefile b/examples/protocols/http_server/simple/Makefile new file mode 100644 index 00000000..48f628a6 --- /dev/null +++ b/examples/protocols/http_server/simple/Makefile @@ -0,0 +1,9 @@ +# +# This is a project Makefile. It is assumed the directory this Makefile resides in is a +# project subdirectory. +# + +PROJECT_NAME := simple + +include $(IDF_PATH)/make/project.mk + diff --git a/examples/protocols/http_server/simple/README.md b/examples/protocols/http_server/simple/README.md new file mode 100644 index 00000000..b9af67ec --- /dev/null +++ b/examples/protocols/http_server/simple/README.md @@ -0,0 +1,27 @@ +# Simple HTTPD Server Example + +The Example consists of HTTPD server demo with demostration of URI handling : + 1. URI \hello for GET command returns "Hello World!" message + 2. URI \echo for POST command echoes back the POSTed message + +* Configure the project using "make menuconfig" and goto : + * Example Configuration -> + 1. WIFI SSID: WIFI network to which your PC is also connected to. + 2. WIFI Password: WIFI password + +* In order to test the HTTPD server persistent sockets demo : + 1. compile and burn the firmware "make flash" + 2. run "make monitor" and note down the IP assigned to your ESP module. The default port is 80 + 3. test the example : + * run the test script : "python2 scripts/client.py \<IP\> \<port\> \<MSG\>" + * the provided test script first does a GET \hello and displays the response + * the script does a POST to \echo with the user input \<MSG\> and displays the response + * or use curl (asssuming IP is 192.168.43.130): + 1. "curl 192.168.43.130:80/hello" - tests the GET "\hello" handler + 2. "curl -X POST --data-binary @anyfile 192.168.43.130:80/echo > tmpfile" + * "anyfile" is the file being sent as request body and "tmpfile" is where the body of the response is saved + * since the server echoes back the request body, the two files should be same, as can be confirmed using : "cmp anyfile tmpfile" + 3. "curl -X PUT -d "0" 192.168.43.130:80/ctrl" - disable /hello and /echo handlers + 4. "curl -X PUT -d "1" 192.168.43.130:80/ctrl" - enable /hello and /echo handlers + +See the README.md file in the upper level 'examples' directory for more information about examples. diff --git a/examples/protocols/http_server/simple/http_server_simple_test.py b/examples/protocols/http_server/simple/http_server_simple_test.py new file mode 100644 index 00000000..b9b71a53 --- /dev/null +++ b/examples/protocols/http_server/simple/http_server_simple_test.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# +# Copyright 2018 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from builtins import range +import imp +import re +import os +import sys +import string +import random +import socket + +# This environment variable is expected on the host machine +test_fw_path = os.getenv("TEST_FW_PATH") +if test_fw_path and test_fw_path not in sys.path: + sys.path.insert(0, test_fw_path) + +# When running on local machine execute the following before running this script +# > make app bootloader +# > make print_flash_cmd | tail -n 1 > build/download.config +# > export TEST_FW_PATH=~/esp/esp-idf/tools/tiny-test-fw + +import TinyFW +import IDF +import Utility + +# Import client module +expath = os.path.dirname(os.path.realpath(__file__)) +client = imp.load_source("client", expath + "/scripts/client.py") + +@IDF.idf_example_test(env_tag="Example_WIFI") +def test_examples_protocol_http_server_simple(env, extra_data): + # Acquire DUT + dut1 = env.get_dut("http_server", "examples/protocols/http_server/simple") + + # Get binary file + binary_file = os.path.join(dut1.app.binary_path, "simple.bin") + bin_size = os.path.getsize(binary_file) + IDF.log_performance("http_server_bin_size", "{}KB".format(bin_size//1024)) + IDF.check_performance("http_server_bin_size", bin_size//1024) + + # Upload binary and start testing + Utility.console_log("Starting http_server simple test app") + dut1.start_app() + + # Parse IP address of STA + Utility.console_log("Waiting to connect with AP") + got_ip = dut1.expect(re.compile(r"(?:[\s\S]*)Got IP: '(\d+.\d+.\d+.\d+)'"), timeout=120)[0] + got_port = dut1.expect(re.compile(r"(?:[\s\S]*)Starting server on port: '(\d+)'"), timeout=30)[0] + + Utility.console_log("Got IP : " + got_ip) + Utility.console_log("Got Port : " + got_port) + + # Expected Logs + dut1.expect("Registering URI handlers", timeout=30) + + # Run test script + # If failed raise appropriate exception + Utility.console_log("Test /hello GET handler") + if not client.test_get_handler(got_ip, got_port): + raise RuntimeError + + # Acquire host IP. Need a way to check it + host_ip = dut1.expect(re.compile(r"(?:[\s\S]*)Found header => Host: (\d+.\d+.\d+.\d+)"), timeout=30)[0] + + # Match additional headers sent in the request + dut1.expect("Found header => Test-Header-2: Test-Value-2", timeout=30) + dut1.expect("Found header => Test-Header-1: Test-Value-1", timeout=30) + dut1.expect("Found URL query parameter => query1=value1", timeout=30) + dut1.expect("Found URL query parameter => query3=value3", timeout=30) + dut1.expect("Found URL query parameter => query2=value2", timeout=30) + dut1.expect("Request headers lost", timeout=30) + + Utility.console_log("Test /ctrl PUT handler and realtime handler de/registration") + if not client.test_put_handler(got_ip, got_port): + raise RuntimeError + dut1.expect("Unregistering /hello and /echo URIs", timeout=30) + dut1.expect("Registering /hello and /echo URIs", timeout=30) + + # Generate random data of 10KB + random_data = ''.join(string.printable[random.randint(0,len(string.printable))-1] for _ in range(10*1024)) + Utility.console_log("Test /echo POST handler with random data") + if not client.test_post_handler(got_ip, got_port, random_data): + raise RuntimeError + + query = "http://foobar" + Utility.console_log("Test /hello with custom query : " + query) + if not client.test_custom_uri_query(got_ip, got_port, query): + raise RuntimeError + dut1.expect("Found URL query => " + query, timeout=30) + + query = "abcd+1234%20xyz" + Utility.console_log("Test /hello with custom query : " + query) + if not client.test_custom_uri_query(got_ip, got_port, query): + raise RuntimeError + dut1.expect("Found URL query => " + query, timeout=30) + + query = "abcd\nyz" + Utility.console_log("Test /hello with invalid query") + if client.test_custom_uri_query(got_ip, got_port, query): + raise RuntimeError + dut1.expect("400 Bad Request - Server unable to understand request due to invalid syntax", timeout=30) + +if __name__ == '__main__': + test_examples_protocol_http_server_simple() diff --git a/examples/protocols/http_server/simple/main/CMakeLists.txt b/examples/protocols/http_server/simple/main/CMakeLists.txt new file mode 100644 index 00000000..85970762 --- /dev/null +++ b/examples/protocols/http_server/simple/main/CMakeLists.txt @@ -0,0 +1,4 @@ +set(COMPONENT_SRCS "main.c") +set(COMPONENT_ADD_INCLUDEDIRS ".") + +register_component() diff --git a/examples/protocols/http_server/simple/main/Kconfig.projbuild b/examples/protocols/http_server/simple/main/Kconfig.projbuild new file mode 100644 index 00000000..9e2813c6 --- /dev/null +++ b/examples/protocols/http_server/simple/main/Kconfig.projbuild @@ -0,0 +1,16 @@ +menu "Example Configuration" + +config WIFI_SSID + string "WiFi SSID" + default "myssid" + help + SSID (network name) for the example to connect to. + +config WIFI_PASSWORD + string "WiFi Password" + default "mypassword" + help + WiFi password (WPA or WPA2) for the example to use. + Can be left blank if the network has no security set. + +endmenu diff --git a/examples/protocols/http_server/simple/main/component.mk b/examples/protocols/http_server/simple/main/component.mk new file mode 100644 index 00000000..0b9d7585 --- /dev/null +++ b/examples/protocols/http_server/simple/main/component.mk @@ -0,0 +1,5 @@ +# +# "main" pseudo-component makefile. +# +# (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) + diff --git a/examples/protocols/http_server/simple/main/main.c b/examples/protocols/http_server/simple/main/main.c new file mode 100644 index 00000000..d4b7b386 --- /dev/null +++ b/examples/protocols/http_server/simple/main/main.c @@ -0,0 +1,279 @@ +/* Simple HTTP Server Example + + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include <esp_wifi.h> +#include <esp_event_loop.h> +#include <esp_log.h> +#include <esp_system.h> +#include <nvs_flash.h> +#include <sys/param.h> + +#include <esp_http_server.h> + +/* A simple example that demonstrates how to create GET and POST + * handlers for the web server. + * The examples use simple WiFi configuration that you can set via + * 'make menuconfig'. + * If you'd rather not, just change the below entries to strings + * with the config you want - + * ie. #define EXAMPLE_WIFI_SSID "mywifissid" +*/ +#define EXAMPLE_WIFI_SSID CONFIG_WIFI_SSID +#define EXAMPLE_WIFI_PASS CONFIG_WIFI_PASSWORD + +static const char *TAG="APP"; + +/* An HTTP GET handler */ +esp_err_t hello_get_handler(httpd_req_t *req) +{ + char* buf; + size_t buf_len; + + /* Get header value string length and allocate memory for length + 1, + * extra byte for null termination */ + buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1; + if (buf_len > 1) { + buf = malloc(buf_len); + /* Copy null terminated value string into buffer */ + if (httpd_req_get_hdr_value_str(req, "Host", buf, buf_len) == ESP_OK) { + ESP_LOGI(TAG, "Found header => Host: %s", buf); + } + free(buf); + } + + buf_len = httpd_req_get_hdr_value_len(req, "Test-Header-2") + 1; + if (buf_len > 1) { + buf = malloc(buf_len); + if (httpd_req_get_hdr_value_str(req, "Test-Header-2", buf, buf_len) == ESP_OK) { + ESP_LOGI(TAG, "Found header => Test-Header-2: %s", buf); + } + free(buf); + } + + buf_len = httpd_req_get_hdr_value_len(req, "Test-Header-1") + 1; + if (buf_len > 1) { + buf = malloc(buf_len); + if (httpd_req_get_hdr_value_str(req, "Test-Header-1", buf, buf_len) == ESP_OK) { + ESP_LOGI(TAG, "Found header => Test-Header-1: %s", buf); + } + free(buf); + } + + /* Read URL query string length and allocate memory for length + 1, + * extra byte for null termination */ + buf_len = httpd_req_get_url_query_len(req) + 1; + if (buf_len > 1) { + buf = malloc(buf_len); + if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) { + ESP_LOGI(TAG, "Found URL query => %s", buf); + char param[32]; + /* Get value of expected key from query string */ + if (httpd_query_key_value(buf, "query1", param, sizeof(param)) == ESP_OK) { + ESP_LOGI(TAG, "Found URL query parameter => query1=%s", param); + } + if (httpd_query_key_value(buf, "query3", param, sizeof(param)) == ESP_OK) { + ESP_LOGI(TAG, "Found URL query parameter => query3=%s", param); + } + if (httpd_query_key_value(buf, "query2", param, sizeof(param)) == ESP_OK) { + ESP_LOGI(TAG, "Found URL query parameter => query2=%s", param); + } + } + free(buf); + } + + /* Set some custom headers */ + httpd_resp_set_hdr(req, "Custom-Header-1", "Custom-Value-1"); + httpd_resp_set_hdr(req, "Custom-Header-2", "Custom-Value-2"); + + /* Send response with custom headers and body set as the + * string passed in user context*/ + const char* resp_str = (const char*) req->user_ctx; + httpd_resp_send(req, resp_str, strlen(resp_str)); + + /* After sending the HTTP response the old HTTP request + * headers are lost. Check if HTTP request headers can be read now. */ + if (httpd_req_get_hdr_value_len(req, "Host") == 0) { + ESP_LOGI(TAG, "Request headers lost"); + } + return ESP_OK; +} + +httpd_uri_t hello = { + .uri = "/hello", + .method = HTTP_GET, + .handler = hello_get_handler, + /* Let's pass response string in user + * context to demonstrate it's usage */ + .user_ctx = "Hello World!" +}; + +/* An HTTP POST handler */ +esp_err_t echo_post_handler(httpd_req_t *req) +{ + char buf[100]; + int ret, remaining = req->content_len; + + while (remaining > 0) { + /* Read the data for the request */ + if ((ret = httpd_req_recv(req, buf, + MIN(remaining, sizeof(buf)))) <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + /* Retry receiving if timeout occurred */ + continue; + } + return ESP_FAIL; + } + + /* Send back the same data */ + httpd_resp_send_chunk(req, buf, ret); + remaining -= ret; + + /* Log data received */ + ESP_LOGI(TAG, "=========== RECEIVED DATA =========="); + ESP_LOGI(TAG, "%.*s", ret, buf); + ESP_LOGI(TAG, "===================================="); + } + + // End response + httpd_resp_send_chunk(req, NULL, 0); + return ESP_OK; +} + +httpd_uri_t echo = { + .uri = "/echo", + .method = HTTP_POST, + .handler = echo_post_handler, + .user_ctx = NULL +}; + +/* An HTTP PUT handler. This demonstrates realtime + * registration and deregistration of URI handlers + */ +esp_err_t ctrl_put_handler(httpd_req_t *req) +{ + char buf; + int ret; + + if ((ret = httpd_req_recv(req, &buf, 1)) <= 0) { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) { + httpd_resp_send_408(req); + } + return ESP_FAIL; + } + + if (buf == '0') { + /* Handler can be unregistered using the uri string */ + ESP_LOGI(TAG, "Unregistering /hello and /echo URIs"); + httpd_unregister_uri(req->handle, "/hello"); + httpd_unregister_uri(req->handle, "/echo"); + } + else { + ESP_LOGI(TAG, "Registering /hello and /echo URIs"); + httpd_register_uri_handler(req->handle, &hello); + httpd_register_uri_handler(req->handle, &echo); + } + + /* Respond with empty body */ + httpd_resp_send(req, NULL, 0); + return ESP_OK; +} + +httpd_uri_t ctrl = { + .uri = "/ctrl", + .method = HTTP_PUT, + .handler = ctrl_put_handler, + .user_ctx = NULL +}; + +httpd_handle_t start_webserver(void) +{ + httpd_handle_t server = NULL; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + // Start the httpd server + ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); + if (httpd_start(&server, &config) == ESP_OK) { + // Set URI handlers + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(server, &hello); + httpd_register_uri_handler(server, &echo); + httpd_register_uri_handler(server, &ctrl); + return server; + } + + ESP_LOGI(TAG, "Error starting server!"); + return NULL; +} + +void stop_webserver(httpd_handle_t server) +{ + // Stop the httpd server + httpd_stop(server); +} + +static esp_err_t event_handler(void *ctx, system_event_t *event) +{ + httpd_handle_t *server = (httpd_handle_t *) ctx; + + switch(event->event_id) { + case SYSTEM_EVENT_STA_START: + ESP_LOGI(TAG, "SYSTEM_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + break; + case SYSTEM_EVENT_STA_GOT_IP: + ESP_LOGI(TAG, "SYSTEM_EVENT_STA_GOT_IP"); + ESP_LOGI(TAG, "Got IP: '%s'", + ip4addr_ntoa(&event->event_info.got_ip.ip_info.ip)); + + /* Start the web server */ + if (*server == NULL) { + *server = start_webserver(); + } + break; + case SYSTEM_EVENT_STA_DISCONNECTED: + ESP_LOGI(TAG, "SYSTEM_EVENT_STA_DISCONNECTED"); + ESP_ERROR_CHECK(esp_wifi_connect()); + + /* Stop the web server */ + if (*server) { + stop_webserver(*server); + *server = NULL; + } + break; + default: + break; + } + return ESP_OK; +} + +static void initialise_wifi(void *arg) +{ + tcpip_adapter_init(); + ESP_ERROR_CHECK(esp_event_loop_init(event_handler, arg)); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + wifi_config_t wifi_config = { + .sta = { + .ssid = EXAMPLE_WIFI_SSID, + .password = EXAMPLE_WIFI_PASS, + }, + }; + ESP_LOGI(TAG, "Setting WiFi configuration SSID %s...", wifi_config.sta.ssid); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); + ESP_ERROR_CHECK(esp_wifi_start()); +} + +void app_main() +{ + static httpd_handle_t server = NULL; + ESP_ERROR_CHECK(nvs_flash_init()); + initialise_wifi(&server); +} diff --git a/examples/protocols/http_server/simple/scripts/client.py b/examples/protocols/http_server/simple/scripts/client.py new file mode 100644 index 00000000..21220b54 --- /dev/null +++ b/examples/protocols/http_server/simple/scripts/client.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# +# Copyright 2018 Espressif Systems (Shanghai) PTE LTD +# +# 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. + +from __future__ import print_function +from __future__ import unicode_literals +from future import standard_library +standard_library.install_aliases() +from builtins import str +import http.client +import argparse + +def verbose_print(verbosity, *args): + if (verbosity): + Utility.console_log(''.join(str(elems) for elems in args)) + +def test_get_handler(ip, port, verbosity = False): + verbose_print(verbosity, "======== GET HANDLER TEST =============") + # Establish HTTP connection + verbose_print(verbosity, "Connecting to => " + ip + ":" + port) + sess = http.client.HTTPConnection(ip + ":" + port, timeout = 15) + + uri = "/hello?query1=value1&query2=value2&query3=value3" + # GET hello response + test_headers = {"Test-Header-1":"Test-Value-1", "Test-Header-2":"Test-Value-2"} + verbose_print(verbosity, "Sending GET to URI : ", uri) + verbose_print(verbosity, "Sending additional headers : ") + for k, v in test_headers.items(): + verbose_print(verbosity, "\t", k, ": ", v) + sess.request("GET", url=uri, headers=test_headers) + resp = sess.getresponse() + resp_hdrs = resp.getheaders() + resp_data = resp.read().decode() + try: + if resp.getheader("Custom-Header-1") != "Custom-Value-1": + return False + if resp.getheader("Custom-Header-2") != "Custom-Value-2": + return False + except: + return False + + verbose_print(verbosity, "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv") + verbose_print(verbosity, "Server response to GET /hello") + verbose_print(verbosity, "Response Headers : ") + for k, v in resp_hdrs: + verbose_print(verbosity, "\t", k, ": ", v) + verbose_print(verbosity, "Response Data : " + resp_data) + verbose_print(verbosity, "========================================\n") + + # Close HTTP connection + sess.close() + return (resp_data == "Hello World!") + +def test_post_handler(ip, port, msg, verbosity = False): + verbose_print(verbosity, "======== POST HANDLER TEST ============") + # Establish HTTP connection + verbose_print(verbosity, "Connecting to => " + ip + ":" + port) + sess = http.client.HTTPConnection(ip + ":" + port, timeout = 15) + + # POST message to /echo and get back response + sess.request("POST", url="/echo", body=msg) + resp = sess.getresponse() + resp_data = resp.read().decode() + verbose_print(verbosity, "Server response to POST /echo (" + msg + ")") + verbose_print(verbosity, "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv") + verbose_print(verbosity, resp_data) + verbose_print(verbosity, "========================================\n") + + # Close HTTP connection + sess.close() + return (resp_data == msg) + +def test_put_handler(ip, port, verbosity = False): + verbose_print(verbosity, "======== PUT HANDLER TEST =============") + # Establish HTTP connection + verbose_print(verbosity, "Connecting to => " + ip + ":" + port) + sess = http.client.HTTPConnection(ip + ":" + port, timeout = 15) + + # PUT message to /ctrl to disable /hello URI handler + verbose_print(verbosity, "Disabling /hello handler") + sess.request("PUT", url="/ctrl", body="0") + resp = sess.getresponse() + resp.read() + + sess.request("GET", url="/hello") + resp = sess.getresponse() + resp_data1 = resp.read().decode() + verbose_print(verbosity, "Response on GET /hello : " + resp_data1) + + # PUT message to /ctrl to enable /hello URI handler + verbose_print(verbosity, "Enabling /hello handler") + sess.request("PUT", url="/ctrl", body="1") + resp = sess.getresponse() + resp.read() + + sess.request("GET", url="/hello") + resp = sess.getresponse() + resp_data2 = resp.read().decode() + verbose_print(verbosity, "Response on GET /hello : " + resp_data2) + + # Close HTTP connection + sess.close() + return ((resp_data2 == "Hello World!") and (resp_data1 == "This URI doesn't exist")) + +def test_custom_uri_query(ip, port, query, verbosity = False): + verbose_print(verbosity, "======== GET HANDLER TEST =============") + # Establish HTTP connection + verbose_print(verbosity, "Connecting to => " + ip + ":" + port) + sess = http.client.HTTPConnection(ip + ":" + port, timeout = 15) + + uri = "/hello?" + query + # GET hello response + verbose_print(verbosity, "Sending GET to URI : ", uri) + sess.request("GET", url=uri, headers={}) + resp = sess.getresponse() + resp_data = resp.read().decode() + + verbose_print(verbosity, "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv") + verbose_print(verbosity, "Server response to GET /hello") + verbose_print(verbosity, "Response Data : " + resp_data) + verbose_print(verbosity, "========================================\n") + + # Close HTTP connection + sess.close() + return (resp_data == "Hello World!") + +if __name__ == '__main__': + # Configure argument parser + parser = argparse.ArgumentParser(description='Run HTTPd Test') + parser.add_argument('IP' , metavar='IP' , type=str, help='Server IP') + parser.add_argument('port', metavar='port', type=str, help='Server port') + parser.add_argument('msg', metavar='message', type=str, help='Message to be sent to server') + args = vars(parser.parse_args()) + + # Get arguments + ip = args['IP'] + port = args['port'] + msg = args['msg'] + + if not test_get_handler (ip, port, True): + Utility.console_log("Failed!") + if not test_post_handler(ip, port, msg, True): + Utility.console_log("Failed!") + if not test_put_handler (ip, port, True): + Utility.console_log("Failed!")