diff --git a/components/esp_http_server/CMakeLists.txt b/components/esp_http_server/CMakeLists.txt index 878034677..bccef191a 100644 --- a/components/esp_http_server/CMakeLists.txt +++ b/components/esp_http_server/CMakeLists.txt @@ -1,13 +1,11 @@ -set(COMPONENT_ADD_INCLUDEDIRS include) -set(COMPONENT_PRIV_INCLUDEDIRS src/port/esp8266 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_PRIV_REQUIRES lwip) -set(COMPONENT_REQUIRES http_parser) - -register_component() +idf_component_register(SRCS "src/httpd_main.c" + "src/httpd_parse.c" + "src/httpd_sess.c" + "src/httpd_txrx.c" + "src/httpd_uri.c" + "src/httpd_ws.c" + "src/util/ctrl_sock.c" + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "src/port/esp8266" "src/util" + REQUIRES nghttp # for http_parser.h + PRIV_REQUIRES lwip mbedtls esp_timer) diff --git a/components/esp_http_server/Kconfig b/components/esp_http_server/Kconfig index 323961ad5..2f8ed780c 100644 --- a/components/esp_http_server/Kconfig +++ b/components/esp_http_server/Kconfig @@ -1,15 +1,48 @@ 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 + 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 + + config HTTPD_ERR_RESP_NO_DELAY + bool "Use TCP_NODELAY socket option when sending HTTP error responses" + default y + help + Using TCP_NODEALY socket option ensures that HTTP error response reaches the client before the + underlying socket is closed. Please note that turning this off may cause multiple test failures + + config HTTPD_PURGE_BUF_LEN + int "Length of temporary buffer for purging data" + default 32 + help + This sets the size of the temporary buffer used to receive and discard any remaining data that is + received from the HTTP client in the request, but not processed as part of the server HTTP request + handler. + + If the remaining data is larger than the available buffer size, the buffer will be filled in multiple + iterations. The buffer should be small enough to fit on the stack, but large enough to avoid excessive + iterations. + + config HTTPD_LOG_PURGE_DATA + bool "Log purged content data at Debug level" + default n + help + Enabling this will log discarded binary HTTP request data at Debug level. + For large content data this may not be desirable as it will clutter the log. + + config HTTPD_WS_SUPPORT + bool "WebSocket server support" + default n + help + This sets the WebSocket server support. endmenu diff --git a/components/esp_http_server/component.mk b/components/esp_http_server/component.mk index d2a9f6feb..54d152a27 100644 --- a/components/esp_http_server/component.mk +++ b/components/esp_http_server/component.mk @@ -2,4 +2,3 @@ COMPONENT_SRCDIRS := src src/util COMPONENT_ADD_INCLUDEDIRS := include COMPONENT_PRIV_INCLUDEDIRS := src/port/esp8266 src/util - diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index 93dae4dc7..8674aab3a 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -49,9 +49,10 @@ initializer that should be kept in sync .global_transport_ctx_free_fn = NULL, \ .open_fn = NULL, \ .close_fn = NULL, \ + .uri_match_fn = NULL \ } -#define ESP_ERR_HTTPD_BASE (0x8000) /*!< Starting number of HTTPD error codes */ +#define ESP_ERR_HTTPD_BASE (0xb000) /*!< 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 */ @@ -61,6 +62,10 @@ initializer that should be kept in sync #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 */ +/* Symbol to be used as length parameter in httpd_resp_send APIs + * for setting buffer length to string length */ +#define HTTPD_RESP_USE_STRLEN -1 + /* ************** Group: Initialization ************** */ /** @name Initialization * APIs related to the Initialization of the web server @@ -82,7 +87,7 @@ typedef enum http_method httpd_method_t; /** * @brief Prototype for freeing context data (if any) - * @param[in] ctx : object to free + * @param[in] ctx object to free */ typedef void (*httpd_free_ctx_fn_t)(void *ctx); @@ -92,9 +97,11 @@ typedef void (*httpd_free_ctx_fn_t)(void *ctx); * 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 + * @param[in] hd server instance + * @param[in] sockfd session socket file descriptor + * @return + * - ESP_OK : On success + * - Any value other than ESP_OK will signal the server to close the socket immediately */ typedef esp_err_t (*httpd_open_func_t)(httpd_handle_t hd, int sockfd); @@ -104,11 +111,26 @@ typedef esp_err_t (*httpd_open_func_t)(httpd_handle_t hd, int sockfd); * @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 + * @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 Function prototype for URI matching. + * + * @param[in] reference_uri URI/template with respect to which the other URI is matched + * @param[in] uri_to_match URI/template being matched to the reference URI/template + * @param[in] match_upto For specifying the actual length of `uri_to_match` up to + * which the matching algorithm is to be applied (The maximum + * value is `strlen(uri_to_match)`, independent of the length + * of `reference_uri`) + * @return true on match + */ +typedef bool (*httpd_uri_match_func_t)(const char *reference_uri, + const char *uri_to_match, + size_t match_upto); + /** * @brief HTTP Server Configuration Structure * @@ -179,6 +201,8 @@ typedef struct httpd_config { * * 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() + * + * Returning a value other than ESP_OK will immediately close the new socket. */ httpd_open_func_t open_fn; @@ -195,6 +219,24 @@ typedef struct httpd_config { * was closed by the network stack - that is, the file descriptor may not be valid anymore. */ httpd_close_func_t close_fn; + + /** + * URI matcher function. + * + * Called when searching for a matching URI: + * 1) whose request handler is to be executed right + * after an HTTP request is successfully parsed + * 2) in order to prevent duplication while registering + * a new URI handler using `httpd_register_uri_handler()` + * + * Available options are: + * 1) NULL : Internally do basic matching using `strncmp()` + * 2) `httpd_uri_match_wildcard()` : URI wildcard matcher + * + * Users can implement their own matching functions (See description + * of the `httpd_uri_match_func_t` function prototype) + */ + httpd_uri_match_func_t uri_match_fn; } httpd_config_t; /** @@ -227,8 +269,8 @@ typedef struct httpd_config { * * @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 + * @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) @@ -324,6 +366,18 @@ typedef struct httpd_req { * function for freeing the session context, please specify that here. */ httpd_free_ctx_fn_t free_ctx; + + /** + * Flag indicating if Session Context changes should be ignored + * + * By default, if you change the sess_ctx in some URI handler, the http server + * will internally free the earlier context (if non NULL), after the URI handler + * returns. If you want to manage the allocation/reallocation/freeing of + * sess_ctx yourself, set this flag to true, so that the server will not + * perform any checks on it. The context will be cleared by the server + * (by calling free_ctx or free()) only if the socket gets closed. + */ + bool ignore_sess_ctx_changes; } httpd_req_t; /** @@ -343,6 +397,25 @@ typedef struct httpd_uri { * Pointer to user context data which will be available to handler */ void *user_ctx; + +#ifdef CONFIG_HTTPD_WS_SUPPORT + /** + * Flag for indicating a WebSocket endpoint. + * If this flag is true, then method must be HTTP_GET. Otherwise the handshake will not be handled. + */ + bool is_websocket; + + /** + * Flag indicating that control frames (PING, PONG, CLOSE) are also passed to the handler + * This is used if a custom processing of the control frames is needed + */ + bool handle_ws_control_frames; + + /** + * Pointer to subprotocol supported by URI + */ + const char *supported_subprotocol; +#endif } httpd_uri_t; /** @@ -433,6 +506,133 @@ esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char* uri); * @} */ +/* ************** Group: HTTP Error ************** */ +/** @name HTTP Error + * Prototype for HTTP errors and error handling functions + * @{ + */ + +/** + * @brief Error codes sent as HTTP response 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_INTERNAL_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 / upgrade field present in headers + */ + HTTPD_400_BAD_REQUEST, + + /* This response means the client must authenticate itself + * to get the requested response. + */ + HTTPD_401_UNAUTHORIZED, + + /* The client does not have access rights to the content, + * so the server is refusing to give the requested resource. + * Unlike 401, the client's identity is known to the server. + */ + HTTPD_403_FORBIDDEN, + + /* 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 immediately 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 CONFIG_HTTPD_MAX_URI_LEN */ + HTTPD_414_URI_TOO_LONG, + + /* Headers section larger than CONFIG_HTTPD_MAX_REQ_HDR_LEN */ + HTTPD_431_REQ_HDR_FIELDS_TOO_LARGE, + + /* Used internally for retrieving the total count of errors */ + HTTPD_ERR_CODE_MAX +} httpd_err_code_t; + +/** + * @brief Function prototype for HTTP error handling. + * + * This function is executed upon HTTP errors generated during + * internal processing of an HTTP request. This is used to override + * the default behavior on error, which is to send HTTP error response + * and close the underlying socket. + * + * @note + * - If implemented, the server will not automatically send out HTTP + * error response codes, therefore, httpd_resp_send_err() must be + * invoked inside this function if user wishes to generate HTTP + * error responses. + * - When invoked, the validity of `uri`, `method`, `content_len` + * and `user_ctx` fields of the httpd_req_t parameter is not + * guaranteed as the HTTP request may be partially received/parsed. + * - The function must return ESP_OK if underlying socket needs to + * be kept open. Any other value will ensure that the socket is + * closed. The return value is ignored when error is of type + * `HTTPD_500_INTERNAL_SERVER_ERROR` and the socket closed anyway. + * + * @param[in] req HTTP request for which the error needs to be handled + * @param[in] error Error type + * + * @return + * - ESP_OK : error handled successful + * - ESP_FAIL : failure indicates that the underlying socket needs to be closed + */ +typedef esp_err_t (*httpd_err_handler_func_t)(httpd_req_t *req, + httpd_err_code_t error); + +/** + * @brief Function for registering HTTP error handlers + * + * This function maps a handler function to any supported error code + * given by `httpd_err_code_t`. See prototype `httpd_err_handler_func_t` + * above for details. + * + * @param[in] handle HTTP server handle + * @param[in] error Error type + * @param[in] handler_fn User implemented handler function + * (Pass NULL to unset any previously set handler) + * + * @return + * - ESP_OK : handler registered successfully + * - ESP_ERR_INVALID_ARG : invalid error code or server handle + */ +esp_err_t httpd_register_err_handler(httpd_handle_t handle, + httpd_err_code_t error, + httpd_err_handler_func_t handler_fn); + +/** End of HTTP Error + * @} + */ + /* ************** Group: TX/RX ************** */ /** @name TX / RX * Prototype for HTTPDs low-level send/recv functions @@ -451,11 +651,11 @@ esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char* uri); * 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 + * @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 @@ -472,11 +672,11 @@ typedef int (*httpd_send_func_t)(httpd_handle_t hd, int sockfd, const char *buf, * 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 + * @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 @@ -494,8 +694,8 @@ typedef int (*httpd_recv_func_t)(httpd_handle_t hd, int sockfd, char *buf, size_ * 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 + * @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 @@ -704,7 +904,10 @@ size_t httpd_req_get_url_query_len(httpd_req_t *r); * 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 + * - Prior to calling this function, one can use httpd_req_get_url_query_len() + * to know the query string length beforehand and hence allocate the buffer + * of right size (usually query string length + 1 for null termination) + * for storing the query string * * @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) @@ -744,6 +947,30 @@ esp_err_t httpd_req_get_url_query_str(httpd_req_t *r, char *buf, size_t buf_len) */ esp_err_t httpd_query_key_value(const char *qry, const char *key, char *val, size_t val_size); +/** + * @brief Test if a URI matches the given wildcard template. + * + * Template may end with "?" to make the previous character optional (typically a slash), + * "*" for a wildcard match, and "?*" to make the previous character optional, and if present, + * allow anything to follow. + * + * Example: + * - * matches everything + * - /foo/? matches /foo and /foo/ + * - /foo/\* (sans the backslash) matches /foo/ and /foo/bar, but not /foo or /fo + * - /foo/?* or /foo/\*? (sans the backslash) matches /foo/, /foo/bar, and also /foo, but not /foox or /fo + * + * The special characters "?" and "*" anywhere else in the template will be taken literally. + * + * @param[in] uri_template URI template (pattern) + * @param[in] uri_to_match URI to be matched + * @param[in] match_upto how many characters of the URI buffer to test + * (there may be trailing query string etc.) + * + * @return true if a match was found + */ +bool httpd_uri_match_wildcard(const char *uri_template, const char *uri_to_match, size_t match_upto); + /** * @brief API to send a complete HTTP response. * @@ -772,7 +999,7 @@ esp_err_t httpd_query_key_value(const char *qry, const char *key, char *val, siz * * @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() + * @param[in] buf_len Length of the buffer, HTTPD_RESP_USE_STRLEN to use strlen() * * @return * - ESP_OK : On successfully sending the response packet @@ -811,7 +1038,7 @@ esp_err_t httpd_resp_send(httpd_req_t *r, const char *buf, ssize_t buf_len); * * @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() + * @param[in] buf_len Length of the buffer, HTTPD_RESP_USE_STRLEN to use strlen() * * @return * - ESP_OK : On successfully sending the response packet chunk @@ -822,6 +1049,48 @@ esp_err_t httpd_resp_send(httpd_req_t *r, const char *buf, ssize_t buf_len); */ esp_err_t httpd_resp_send_chunk(httpd_req_t *r, const char *buf, ssize_t buf_len); +/** + * @brief API to send a complete string as HTTP response. + * + * This API simply calls http_resp_send with buffer length + * set to string length assuming the buffer contains a null + * terminated string + * + * @param[in] r The request being responded to + * @param[in] str String to be sent as response body + * + * @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 + */ +static inline esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *str) { + return httpd_resp_send(r, str, (str == NULL) ? 0 : HTTPD_RESP_USE_STRLEN); +} + +/** + * @brief API to send a string as an HTTP response chunk. + * + * This API simply calls http_resp_send_chunk with buffer length + * set to string length assuming the buffer contains a null + * terminated string + * + * @param[in] r The request being responded to + * @param[in] str String to be sent as response body (NULL to finish response packet) + * + * @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 + */ +static inline esp_err_t httpd_resp_sendstr_chunk(httpd_req_t *r, const char *str) { + return httpd_resp_send_chunk(r, str, (str == NULL) ? 0 : HTTPD_RESP_USE_STRLEN); +} + /* Some commonly used status codes */ #define HTTPD_200 "200 OK" /*!< HTTP Response 200 */ #define HTTPD_204 "204 No Content" /*!< HTTP Response 204 */ @@ -910,6 +1179,30 @@ esp_err_t httpd_resp_set_type(httpd_req_t *r, const char *type); */ esp_err_t httpd_resp_set_hdr(httpd_req_t *r, const char *field, const char *value); +/** + * @brief For sending out error code in response to HTTP request. + * + * @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. + * - If you wish to send additional data in the body of the + * response, please use the lower-level functions directly. + * + * @param[in] req Pointer to the HTTP request for which the response needs to be sent + * @param[in] error Error type to send + * @param[in] msg Error message string (pass NULL for default message) + * + * @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_err(httpd_req_t *req, httpd_err_code_t error, const char *msg); + /** * @brief Helper function for HTTP 404 * @@ -931,7 +1224,9 @@ esp_err_t httpd_resp_set_hdr(httpd_req_t *r, const char *field, const char *valu * - 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); +static inline esp_err_t httpd_resp_send_404(httpd_req_t *r) { + return httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, NULL); +} /** * @brief Helper function for HTTP 408 @@ -954,7 +1249,9 @@ esp_err_t httpd_resp_send_404(httpd_req_t *r); * - 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); +static inline esp_err_t httpd_resp_send_408(httpd_req_t *r) { + return httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT, NULL); +} /** * @brief Helper function for HTTP 500 @@ -977,7 +1274,9 @@ esp_err_t httpd_resp_send_408(httpd_req_t *r); * - 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); +static inline esp_err_t httpd_resp_send_500(httpd_req_t *r) { + return httpd_resp_send_err(r, HTTPD_500_INTERNAL_SERVER_ERROR, NULL); +} /** * @brief Raw HTTP send @@ -1013,6 +1312,53 @@ esp_err_t httpd_resp_send_500(httpd_req_t *r); */ int httpd_send(httpd_req_t *r, const char *buf, size_t buf_len); +/** + * A low level API to send data on a given socket + * + * @note This API is not recommended to be used in any request handler. + * Use this only for advanced use cases, wherein some asynchronous + * data is to be sent over a socket. + * + * This internally calls the default send function, or the function registered by + * httpd_sess_set_send_override(). + * + * @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() + */ +int httpd_socket_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags); + +/** + * A low level API to receive data from a given socket + * + * @note This API is not recommended to be used in any request handler. + * Use this only for advanced use cases, wherein some asynchronous + * communication is required. + * + * This internally calls the default recv function, or the function registered by + * httpd_sess_set_recv_override(). + * + * @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() + */ +int httpd_socket_recv(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, int flags); + /** End of Request / Response * @} */ @@ -1140,6 +1486,20 @@ esp_err_t httpd_sess_trigger_close(httpd_handle_t handle, int sockfd); */ esp_err_t httpd_sess_update_lru_counter(httpd_handle_t handle, int sockfd); +/** + * @brief Returns list of current socket descriptors of active sessions + * + * @param[in] handle Handle to server returned by httpd_start + * @param[in,out] fds In: Number of fds allocated in the supplied structure client_fds + * Out: Number of valid client fds returned in client_fds, + * @param[out] client_fds Array of client fds + * + * @return + * - ESP_OK : Successfully retrieved session list + * - ESP_ERR_INVALID_ARG : Wrong arguments or list is longer than allocated + */ +esp_err_t httpd_get_client_list(httpd_handle_t handle, size_t *fds, int *client_fds); + /** End of Session * @} */ @@ -1181,6 +1541,112 @@ esp_err_t httpd_queue_work(httpd_handle_t handle, httpd_work_fn_t work, void *ar * @} */ +/* ************** Group: WebSocket ************** */ +/** @name WebSocket + * Functions and structs for WebSocket server + * @{ + */ +#ifdef CONFIG_HTTPD_WS_SUPPORT +/** + * @brief Enum for WebSocket packet types (Opcode in the header) + * @note Please refer to RFC6455 Section 5.4 for more details + */ +typedef enum { + HTTPD_WS_TYPE_CONTINUE = 0x0, + HTTPD_WS_TYPE_TEXT = 0x1, + HTTPD_WS_TYPE_BINARY = 0x2, + HTTPD_WS_TYPE_CLOSE = 0x8, + HTTPD_WS_TYPE_PING = 0x9, + HTTPD_WS_TYPE_PONG = 0xA +} httpd_ws_type_t; + +/** + * @brief Enum for client info description + */ +typedef enum { + HTTPD_WS_CLIENT_INVALID = 0x0, + HTTPD_WS_CLIENT_HTTP = 0x1, + HTTPD_WS_CLIENT_WEBSOCKET = 0x2, +} httpd_ws_client_info_t; + +/** + * @brief WebSocket frame format + */ +typedef struct httpd_ws_frame { + bool final; /*!< Final frame: + For received frames this field indicates whether the `FIN` flag was set. + For frames to be transmitted, this field is only used if the `fragmented` + option is set as well. If `fragmented` is false, the `FIN` flag is set + by default, marking the ws_frame as a complete/unfragmented message + (esp_http_server doesn't automatically fragment messages) */ + bool fragmented; /*!< Indication that the frame allocated for transmission is a message fragment, + so the `FIN` flag is set manually according to the `final` option. + This flag is never set for received messages */ + httpd_ws_type_t type; /*!< WebSocket frame type */ + uint8_t *payload; /*!< Pre-allocated data buffer */ + size_t len; /*!< Length of the WebSocket data */ +} httpd_ws_frame_t; + +/** + * @brief Receive and parse a WebSocket frame + * @param[in] req Current request + * @param[out] pkt WebSocket packet + * @param[in] max_len Maximum length for receive + * @return + * - ESP_OK : On successful + * - ESP_FAIL : Socket errors occurs + * - ESP_ERR_INVALID_STATE : Handshake was already done beforehand + * - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket) + */ +esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *pkt, size_t max_len); + +/** + * @brief Construct and send a WebSocket frame + * @param[in] req Current request + * @param[in] pkt WebSocket frame + * @return + * - ESP_OK : On successful + * - ESP_FAIL : When socket errors occurs + * - ESP_ERR_INVALID_STATE : Handshake was already done beforehand + * - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket) + */ +esp_err_t httpd_ws_send_frame(httpd_req_t *req, httpd_ws_frame_t *pkt); + +/** + * @brief Low level send of a WebSocket frame out of the scope of current request + * using internally configured httpd send function + * + * This API should rarely be called directly, with an exception of asynchronous send using httpd_queue_work. + * + * @param[in] hd Server instance data + * @param[in] fd Socket descriptor for sending data + * @param[in] frame WebSocket frame + * @return + * - ESP_OK : On successful + * - ESP_FAIL : When socket errors occurs + * - ESP_ERR_INVALID_STATE : Handshake was already done beforehand + * - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket) + */ +esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t *frame); + +/** + * @brief Checks the supplied socket descriptor if it belongs to any active client + * of this server instance and if the websoket protocol is active + * + * @param[in] hd Server instance data + * @param[in] fd Socket descriptor + * @return + * - HTTPD_WS_CLIENT_INVALID : This fd is not a client of this httpd + * - HTTPD_WS_CLIENT_HTTP : This fd is an active client, protocol is not WS + * - HTTPD_WS_CLIENT_WEBSOCKET : This fd is an active client, protocol is WS + */ +httpd_ws_client_info_t httpd_ws_get_fd_info(httpd_handle_t hd, int fd); + +#endif /* CONFIG_HTTPD_WS_SUPPORT */ +/** End of WebSocket related stuff + * @} + */ + #ifdef __cplusplus } #endif diff --git a/components/esp_http_server/src/esp_httpd_priv.h b/components/esp_http_server/src/esp_httpd_priv.h index 8cbd0e94c..f4fa57e5a 100644 --- a/components/esp_http_server/src/esp_httpd_priv.h +++ b/components/esp_http_server/src/esp_httpd_priv.h @@ -32,7 +32,7 @@ extern "C" { /* 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 */ + * exceed the scratch buffer size and should at least be 8 bytes */ #define PARSER_BLOCK_SIZE 128 /* Calculate the maximum size needed for the scratch buffer */ @@ -54,70 +54,13 @@ struct thread_data { } 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 */ + bool ignore_sess_ctx_changes; /*!< Flag indicating if session context changes should be ignored */ 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 */ @@ -126,12 +69,19 @@ struct sock_db { httpd_recv_func_t recv_fn; /*!< Receive function for this socket */ httpd_pending_func_t pending_fn; /*!< Pending function for this socket */ uint64_t lru_counter; /*!< LRU Counter indicating when the socket was last used */ + bool lru_socket; /*!< Flag indicating LRU socket */ char pending_data[PARSER_BLOCK_SIZE]; /*!< Buffer for pending data to be received */ size_t pending_len; /*!< Length of pending data to be received */ +#ifdef CONFIG_HTTPD_WS_SUPPORT + bool ws_handshake_done; /*!< True if it has done WebSocket handshake (if this socket is a valid WS) */ + bool ws_close; /*!< Set to true to close the socket later (when WS Close frame received) */ + esp_err_t (*ws_handler)(httpd_req_t *r); /*!< WebSocket handler, leave to null if it's not WebSocket */ + bool ws_control_frames; /*!< WebSocket flag indicating that control frames should be passed to user handlers */ +#endif }; /** - * @brief Auxilary data structure for use during reception and processing + * @brief Auxiliary data structure for use during reception and processing * of requests and temporarily keeping responses */ struct httpd_req_aux { @@ -148,10 +98,15 @@ struct httpd_req_aux { 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 */ +#ifdef CONFIG_HTTPD_WS_SUPPORT + bool ws_handshake_detect; /*!< WebSocket handshake detection flag */ + httpd_ws_type_t ws_type; /*!< WebSocket frame type */ + bool ws_final; /*!< WebSocket FIN bit (final frame or not) */ +#endif }; /** - * @brief Server data for each instance. This is exposed publicaly as + * @brief Server data for each instance. This is exposed publicly as * httpd_handle_t but internal structure/members are kept private. */ struct httpd_data { @@ -159,11 +114,14 @@ struct httpd_data { 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 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 */ + + /* Array of registered error handler functions */ + httpd_err_handler_func_t *err_handler_fns; }; /******************* Group : Session Management ********************/ @@ -204,7 +162,7 @@ void httpd_sess_init(struct httpd_data *hd); * @param[in] newfd Descriptor of the new client to be added to the session. * * @return - * - ESP_OK : on successfully queueing the work + * - ESP_OK : on successfully queuing the work * - ESP_FAIL : in case of control socket error while sending */ esp_err_t httpd_sess_new(struct httpd_data *hd, int newfd); @@ -226,7 +184,7 @@ esp_err_t httpd_sess_process(struct httpd_data *hd, int clifd); * 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 + * to continue the iteration correctly. This ensures 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. @@ -249,7 +207,7 @@ int httpd_sess_delete(struct httpd_data *hd, int clifd); 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 + * @brief Add descriptors present in the socket database to an fdset and * update the value of maxfd which are needed by the select function * for looking through all available sockets for incoming data. * @@ -288,12 +246,12 @@ 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 + * This is needed as httpd_unrecv may un-receive 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 + * then it would not be processed until further data is * received on the socket. This is when this function * comes in use, as it checks the socket's pending data * buffer. @@ -343,7 +301,7 @@ esp_err_t httpd_sess_close_lru(struct httpd_data *hd); esp_err_t httpd_uri(struct httpd_data *hd); /** - * @brief Deregister all URI handlers + * @brief Unregister all URI handlers * * @param[in] hd Server instance data */ @@ -353,7 +311,7 @@ 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 + * @param[in] req Pointer to HTTP request that needs to be validated * * @return * - true : if valid request @@ -363,7 +321,7 @@ 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 */ + * the check by default and enable it only if necessary */ #ifdef CONFIG_HTTPD_VALIDATE_REQ #define httpd_valid_req(r) httpd_validate_req_ptr(r) #else @@ -409,6 +367,19 @@ esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd); */ esp_err_t httpd_req_delete(struct httpd_data *hd); +/** + * @brief For handling HTTP errors by invoking registered + * error handler function + * + * @param[in] req Pointer to the HTTP request for which error occurred + * @param[in] error Error type + * + * @return + * - ESP_OK : error handled successful + * - ESP_FAIL : failure indicates that the underlying socket needs to be closed + */ +esp_err_t httpd_req_handle_err(httpd_req_t *req, httpd_err_code_t error); + /** End of Group : Parsing * @} */ @@ -419,22 +390,10 @@ esp_err_t httpd_req_delete(struct httpd_data *hd); * @{ */ -/** - * @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] req Pointer to the HTTP request for which the response 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 * @@ -457,7 +416,7 @@ int httpd_send(httpd_req_t *req, const char *buf, size_t buf_len); * @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 + * @param[in] halt_after_pending When set true, halts immediately after receiving from * pending buffer * * @return @@ -524,6 +483,45 @@ int httpd_default_recv(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, * @} */ +/* ************** Group: WebSocket ************** */ +/** @name WebSocket + * Functions for WebSocket header parsing + * @{ + */ + + +/** + * @brief This function is for responding a WebSocket handshake + * + * @param[in] req Pointer to handshake request that will be handled + * @param[in] supported_subprotocol Pointer to the subprotocol supported by this URI + * @return + * - ESP_OK : When handshake is sucessful + * - ESP_ERR_NOT_FOUND : When some headers (Sec-WebSocket-*) are not found + * - ESP_ERR_INVALID_VERSION : The WebSocket version is not "13" + * - ESP_ERR_INVALID_STATE : Handshake was done beforehand + * - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket) + * - ESP_FAIL : Socket failures + */ +esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *supported_subprotocol); + +/** + * @brief This function is for getting a frame type + * and responding a WebSocket control frame automatically + * + * @param[in] req Pointer to handshake request that will be handled + * @return + * - ESP_OK : When handshake is sucessful + * - ESP_ERR_INVALID_ARG : Argument is invalid (null or non-WebSocket) + * - ESP_ERR_INVALID_STATE : Received only some parts of a control frame + * - ESP_FAIL : Socket failures + */ +esp_err_t httpd_ws_get_frame_type(httpd_req_t *req); + +/** End of WebSocket related functions + * @} + */ + #ifdef __cplusplus } #endif diff --git a/components/esp_http_server/src/httpd_main.c b/components/esp_http_server/src/httpd_main.c index a67513195..da8262bca 100644 --- a/components/esp_http_server/src/httpd_main.c +++ b/components/esp_http_server/src/httpd_main.c @@ -102,6 +102,26 @@ esp_err_t httpd_queue_work(httpd_handle_t handle, httpd_work_fn_t work, void *ar return ESP_OK; } +esp_err_t httpd_get_client_list(httpd_handle_t handle, size_t *fds, int *client_fds) +{ + struct httpd_data *hd = (struct httpd_data *) handle; + if (hd == NULL || fds == NULL || *fds == 0 || client_fds == NULL || *fds < hd->config.max_open_sockets) { + return ESP_ERR_INVALID_ARG; + } + size_t max_fds = *fds; + *fds = 0; + for (int i = 0; i < hd->config.max_open_sockets; ++i) { + if (hd->hd_sd[i].fd != -1) { + if (*fds < max_fds) { + client_fds[(*fds)++] = hd->hd_sd[i].fd; + } else { + return ESP_ERR_INVALID_ARG; + } + } + } + return ESP_OK; +} + void *httpd_get_global_user_ctx(httpd_handle_t handle) { return ((struct httpd_data *)handle)->config.global_user_ctx; @@ -156,7 +176,12 @@ 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); + if (hd->config.lru_purge_enable || httpd_is_sess_available(hd)) { + /* Only listen for new connections if server has capacity to + * handle more (or when LRU purge is enabled, in which case + * older connections will be closed) */ + FD_SET(hd->listen_fd, &read_set); + } FD_SET(hd->ctrl_fd, &read_set); int tmp_max_fd; @@ -236,17 +261,16 @@ static void httpd_thread(void *arg) static esp_err_t httpd_server_init(struct httpd_data *hd) { -#ifdef CONFIG_LWIP_IPV6 +#if CONFIG_LWIP_IPV6 int fd = socket(PF_INET6, SOCK_STREAM, 0); #else int fd = socket(PF_INET, SOCK_STREAM, 0); -#endif /* CONFIG_LWIP_IPV6 */ +#endif if (fd < 0) { ESP_LOGE(TAG, LOG_FMT("error in socket (%d)"), errno); return ESP_FAIL; } - -#ifdef CONFIG_LWIP_IPV6 +#if CONFIG_LWIP_IPV6 struct in6_addr inaddr_any = IN6ADDR_ANY_INIT; struct sockaddr_in6 serv_addr = { .sin6_family = PF_INET6, @@ -261,7 +285,16 @@ static esp_err_t httpd_server_init(struct httpd_data *hd) }, .sin_port = htons(hd->config.server_port) }; -#endif /* CONFIG_LWIP_IPV6 */ +#endif + /* Enable SO_REUSEADDR to allow binding to the same + * address and port when restarting the server */ + int enable = 1; + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) { + /* This will fail if CONFIG_LWIP_SO_REUSE is not enabled. But + * it does not affect the normal working of the HTTP Server */ + ESP_LOGW(TAG, LOG_FMT("error enabling SO_REUSEADDR (%d)"), errno); + } + int ret = bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); if (ret < 0) { ESP_LOGE(TAG, LOG_FMT("error in bind (%d)"), errno); @@ -301,31 +334,43 @@ 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"); + if (!hd) { + ESP_LOGE(TAG, LOG_FMT("Failed to allocate memory for HTTP server instance")); + return NULL; + } + hd->hd_calls = calloc(config->max_uri_handlers, sizeof(httpd_uri_t *)); + if (!hd->hd_calls) { + ESP_LOGE(TAG, LOG_FMT("Failed to allocate memory for HTTP URI handlers")); + free(hd); + return NULL; + } + hd->hd_sd = calloc(config->max_open_sockets, sizeof(struct sock_db)); + if (!hd->hd_sd) { + ESP_LOGE(TAG, LOG_FMT("Failed to allocate memory for HTTP session data")); + 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) { + ESP_LOGE(TAG, LOG_FMT("Failed to allocate memory for HTTP response headers")); + free(hd->hd_sd); + free(hd->hd_calls); + free(hd); + return NULL; + } + hd->err_handler_fns = calloc(HTTPD_ERR_CODE_MAX, sizeof(httpd_err_handler_func_t)); + if (!hd->err_handler_fns) { + ESP_LOGE(TAG, LOG_FMT("Failed to allocate memory for HTTP error handlers")); + free(ra->resp_hdrs); + free(hd->hd_sd); + free(hd->hd_calls); + free(hd); + return NULL; + } + /* Save the configuration for this instance */ + hd->config = *config; return hd; } @@ -333,6 +378,7 @@ static void httpd_delete(struct httpd_data *hd) { struct httpd_req_aux *ra = &hd->hd_req_aux; /* Free memory of httpd instance data */ + free(hd->err_handler_fns); free(ra->resp_hdrs); free(hd->hd_sd); @@ -348,6 +394,23 @@ esp_err_t httpd_start(httpd_handle_t *handle, const httpd_config_t *config) return ESP_ERR_INVALID_ARG; } + /* Sanity check about whether LWIP is configured for providing the + * maximum number of open sockets sufficient for the server. Though, + * this check doesn't guarantee that many sockets will actually be + * available at runtime as other processes may use up some sockets. + * Note that server also uses 3 sockets for its internal use : + * 1) listening for new TCP connections + * 2) for sending control messages over UDP + * 3) for receiving control messages over UDP + * So the total number of required sockets is max_open_sockets + 3 + */ + if (CONFIG_LWIP_MAX_SOCKETS < config->max_open_sockets + 3) { + ESP_LOGE(TAG, "Configuration option max_open_sockets is too large (max allowed %d)\n\t" + "Either decrease this or configure LWIP_MAX_SOCKETS to a larger value", + CONFIG_LWIP_MAX_SOCKETS - 3); + return ESP_ERR_INVALID_ARG; + } + struct httpd_data *hd = httpd_create(config); if (hd == NULL) { /* Failed to allocate memory */ diff --git a/components/esp_http_server/src/httpd_parse.c b/components/esp_http_server/src/httpd_parse.c index 51843182d..cc328a9e6 100644 --- a/components/esp_http_server/src/httpd_parse.c +++ b/components/esp_http_server/src/httpd_parse.c @@ -46,7 +46,7 @@ typedef struct { } status; /* Response error code in case of PARSING_FAILED */ - httpd_err_resp_t error; + httpd_err_code_t error; /* For storing last callback parameters */ struct { @@ -81,7 +81,6 @@ static esp_err_t verify_url (http_parser *parser) 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; } @@ -128,6 +127,7 @@ static esp_err_t cb_url(http_parser *parser, parser_data->status = PARSING_URL; } else if (parser_data->status != PARSING_URL) { ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR; parser_data->status = PARSING_FAILED; return ESP_FAIL; } @@ -151,15 +151,28 @@ static esp_err_t pause_parsing(http_parser *parser, const char* at) 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); + /* The length of data that was not parsed due to interruption + * and hence needs to be read again later for parsing */ + ssize_t unparsed = parser_data->raw_datalen - (at - ra->scratch); + if (unparsed < 0) { + ESP_LOGE(TAG, LOG_FMT("parsing beyond valid data = %d"), -unparsed); + return ESP_ERR_INVALID_STATE; + } - 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); + /* Push back the un-parsed data into pending buffer for + * receiving again with httpd_recv_with_opt() later when + * read_block() executes */ + if (unparsed && (unparsed != httpd_unrecv(r, at, unparsed))) { + ESP_LOGE(TAG, LOG_FMT("data too large for un-recv = %d"), unparsed); return ESP_FAIL; } + /* Signal http_parser to pause execution and save the maximum + * possible length, of the yet un-parsed data, that may get + * parsed before http_parser_execute() returns. This pre_parsed + * length will be updated then to reflect the actual length + * that got parsed, and must be skipped when parsing resumes */ + parser_data->pre_parsed = unparsed; http_parser_pause(parser, 1); parser_data->paused = true; ESP_LOGD(TAG, LOG_FMT("paused")); @@ -170,8 +183,8 @@ 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 */ + /* Part of the received data may have been parsed earlier + * so we must skip that before parsing resumes */ length = MIN(length, data->pre_parsed); data->pre_parsed -= length; ESP_LOGD(TAG, LOG_FMT("skip pre-parsed data of size = %d"), length); @@ -194,6 +207,9 @@ static esp_err_t cb_header_field(http_parser *parser, const char *at, size_t len /* Check previous status */ if (parser_data->status == PARSING_URL) { if (verify_url(parser) != ESP_OK) { + /* verify_url would already have set the + * error field of parser data, so only setting + * status to failed */ parser_data->status = PARSING_FAILED; return ESP_FAIL; } @@ -207,20 +223,26 @@ static esp_err_t cb_header_field(http_parser *parser, const char *at, size_t len /* Stop parsing for now and give control to process */ if (pause_parsing(parser, at) != ESP_OK) { + parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR; 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'; + /* Overwrite terminator (CRLFs) following last header + * (key: value) pair with null characters */ + char *term_start = (char *)parser_data->last.at + parser_data->last.length; + memset(term_start, '\0', at - term_start); /* Store current values of the parser callback arguments */ parser_data->last.at = at; parser_data->last.length = 0; parser_data->status = PARSING_HDR_FIELD; + + /* Increment header count */ + ra->req_hdrs_count++; } else if (parser_data->status != PARSING_HDR_FIELD) { ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR; parser_data->status = PARSING_FAILED; return ESP_FAIL; } @@ -238,8 +260,6 @@ static esp_err_t cb_header_field(http_parser *parser, const char *at, size_t len 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) { @@ -247,10 +267,26 @@ static esp_err_t cb_header_value(http_parser *parser, const char *at, size_t len parser_data->last.at = at; parser_data->last.length = 0; parser_data->status = PARSING_HDR_VALUE; - /* Increment header count */ - ra->req_hdrs_count++; + + if (length == 0) { + /* As per behavior of http_parser, when length > 0, + * `at` points to the start of CRLF. But, in the + * case when header value is empty (zero length), + * then `at` points to the position right after + * the CRLF. Since for our purpose we need `last.at` + * to point to exactly where the CRLF starts, it + * needs to be adjusted by the right offset */ + char *at_adj = (char *)parser_data->last.at; + /* Find the end of header field string */ + while (*(--at_adj) != ':'); + /* Now skip leading spaces' */ + while (*(++at_adj) == ' '); + /* Now we are at the right position */ + parser_data->last.at = at_adj; + } } else if (parser_data->status != PARSING_HDR_VALUE) { ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR; parser_data->status = PARSING_FAILED; return ESP_FAIL; } @@ -275,18 +311,58 @@ static esp_err_t cb_headers_complete(http_parser *parser) if (parser_data->status == PARSING_URL) { ESP_LOGD(TAG, LOG_FMT("no headers")); if (verify_url(parser) != ESP_OK) { + /* verify_url would already have set the + * error field of parser data, so only setting + * status to failed */ 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'; + /* Locate end of last header */ + char *at = (char *)parser_data->last.at + parser_data->last.length; + + /* Check if there is data left to parse. This value should + * at least be equal to the number of line terminators, i.e. 2 */ + ssize_t remaining_length = parser_data->raw_datalen - (at - ra->scratch); + if (remaining_length < 2) { + ESP_LOGE(TAG, LOG_FMT("invalid length of data remaining to be parsed")); + parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR; + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } - /* Reach end of last header */ - parser_data->last.at += parser_data->last.length; + /* Locate end of headers section by skipping the remaining + * two line terminators. No assumption is made here about the + * termination sequence used apart from the necessity that it + * must end with an LF, because: + * 1) some clients may send non standard LFs instead of + * CRLFs for indicating termination. + * 2) it is the responsibility of http_parser to check + * that the termination is either CRLF or LF and + * not any other sequence */ + unsigned short remaining_terminators = 2; + while (remaining_length-- && remaining_terminators) { + if (*at == '\n') { + remaining_terminators--; + } + /* Overwrite termination characters with null */ + *(at++) = '\0'; + } + if (remaining_terminators) { + ESP_LOGE(TAG, LOG_FMT("incomplete termination of headers")); + parser_data->error = HTTPD_400_BAD_REQUEST; + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + /* Place the parser ptr right after the end of headers section */ + parser_data->last.at = at; + + /* Increment header count */ + ra->req_hdrs_count++; } else { ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR; parser_data->status = PARSING_FAILED; return ESP_FAIL; } @@ -298,11 +374,36 @@ static esp_err_t cb_headers_complete(http_parser *parser) ESP_LOGD(TAG, LOG_FMT("bytes read = %d"), parser->nread); ESP_LOGD(TAG, LOG_FMT("content length = %zu"), r->content_len); + /* Handle upgrade requests - only WebSocket is supported for now */ if (parser->upgrade) { - ESP_LOGW(TAG, LOG_FMT("upgrade from HTTP not supported")); - parser_data->error = HTTPD_XXX_UPGRADE_NOT_SUPPORTED; +#ifdef CONFIG_HTTPD_WS_SUPPORT + ESP_LOGD(TAG, LOG_FMT("Got an upgrade request")); + + /* If there's no "Upgrade" header field, then it's not WebSocket. */ + char ws_upgrade_hdr_val[] = "websocket"; + if (httpd_req_get_hdr_value_str(r, "Upgrade", ws_upgrade_hdr_val, sizeof(ws_upgrade_hdr_val)) != ESP_OK) { + ESP_LOGW(TAG, LOG_FMT("Upgrade header does not match the length of \"websocket\"")); + parser_data->error = HTTPD_400_BAD_REQUEST; + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + /* If "Upgrade" field's key is not "websocket", then we should also forget about it. */ + if (strcasecmp("websocket", ws_upgrade_hdr_val) != 0) { + ESP_LOGW(TAG, LOG_FMT("Upgrade header found but it's %s"), ws_upgrade_hdr_val); + parser_data->error = HTTPD_400_BAD_REQUEST; + parser_data->status = PARSING_FAILED; + return ESP_FAIL; + } + + /* Now set handshake flag to true */ + ra->ws_handshake_detect = true; +#else + ESP_LOGD(TAG, LOG_FMT("WS functions has been disabled, Upgrade request is not supported.")); + parser_data->error = HTTPD_400_BAD_REQUEST; parser_data->status = PARSING_FAILED; return ESP_FAIL; +#endif } parser_data->status = PARSING_BODY; @@ -320,6 +421,7 @@ static esp_err_t cb_on_body(http_parser *parser, const char *at, size_t length) /* Check previous status */ if (parser_data->status != PARSING_BODY) { ESP_LOGE(TAG, LOG_FMT("unexpected state transition")); + parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR; parser_data->status = PARSING_FAILED; return ESP_FAIL; } @@ -329,6 +431,7 @@ static esp_err_t cb_on_body(http_parser *parser, const char *at, size_t length) * may reset the parser state and cause current * request packet to be lost */ if (pause_parsing(parser, at) != ESP_OK) { + parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR; parser_data->status = PARSING_FAILED; return ESP_FAIL; } @@ -346,29 +449,30 @@ static esp_err_t cb_on_body(http_parser *parser, const char *at, size_t length) 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) { + /* verify_url would already have set the + * error field of parser data, so only setting + * status to failed */ 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->error = HTTPD_500_INTERNAL_SERVER_ERROR; 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) { + if (pause_parsing(parser, parser_data->last.at) != ESP_OK) { + parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR; parser_data->status = PARSING_FAILED; return ESP_FAIL; } @@ -385,8 +489,8 @@ 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) { + ssize_t buf_len = MIN(length, (sizeof(raux->scratch) - offset)); + if (buf_len <= 0) { return 0; } @@ -396,13 +500,25 @@ static int read_block(httpd_req_t *req, size_t offset, size_t length) 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 timeout occurred allow the + * situation to be handled */ if (nbytes == HTTPD_SOCK_ERR_TIMEOUT) { - httpd_resp_send_err(req, HTTPD_408_REQ_TIMEOUT); + /* Invoke error handler which may return ESP_OK + * to signal for retrying call to recv(), else it may + * return ESP_FAIL to signal for closure of socket */ + return (httpd_req_handle_err(req, HTTPD_408_REQ_TIMEOUT) == ESP_OK) ? + HTTPD_SOCK_ERR_TIMEOUT : HTTPD_SOCK_ERR_FAIL; } - return -1; + /* Some socket error occurred. Return failure + * to force closure of underlying socket. + * Error message is not sent as socket may not + * be valid anymore */ + return HTTPD_SOCK_ERR_FAIL; } else if (nbytes == 0) { ESP_LOGD(TAG, LOG_FMT("connection closed")); - return -1; + /* Connection closed by client so no + * need to send error response */ + return HTTPD_SOCK_ERR_FAIL; } ESP_LOGD(TAG, LOG_FMT("received HTTP request block size = %d"), nbytes); @@ -417,7 +533,11 @@ static int parse_block(http_parser *parser, size_t offset, size_t length) size_t nparsed = 0; if (!length) { - ESP_LOGW(TAG, LOG_FMT("response uri/header too big")); + /* Parsing is still happening but nothing to + * parse means no more space left on buffer, + * therefore it can be inferred that the + * request URI/header must be too long */ + ESP_LOGW(TAG, LOG_FMT("request URI/header too long")); switch (data->status) { case PARSING_URL: data->error = HTTPD_414_URI_TOO_LONG; @@ -425,14 +545,17 @@ static int parse_block(http_parser *parser, size_t offset, size_t length) case PARSING_HDR_FIELD: case PARSING_HDR_VALUE: data->error = HTTPD_431_REQ_HDR_FIELDS_TOO_LARGE; + break; default: + ESP_LOGE(TAG, LOG_FMT("unexpected state")); + data->error = HTTPD_500_INTERNAL_SERVER_ERROR; break; } data->status = PARSING_FAILED; return -1; } - /* Unpause the parsing if paused */ + /* Un-pause the parsing if paused */ if (data->paused) { nparsed = continue_parsing(parser, length); length -= nparsed; @@ -448,23 +571,29 @@ static int parse_block(http_parser *parser, size_t offset, size_t length) /* Check state */ if (data->status == PARSING_FAILED) { + /* It is expected that the error field of + * parser data should have been set by now */ 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 */ + /* Update the value of pre_parsed which was set when + * pause_parsing() was called. (length - nparsed) is + * the length of the data that will need to be parsed + * again later and hence must be deducted from the + * pre_parsed length */ data->pre_parsed -= (length - nparsed); return 0; } else if (nparsed != length) { /* http_parser error */ - data->status = PARSING_FAILED; data->error = HTTPD_400_BAD_REQUEST; + data->status = PARSING_FAILED; 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 */ + /* Return with the total length of the request packet + * that has been parsed till now */ ESP_LOGD(TAG, LOG_FMT("parsed block size = %d"), offset + nparsed); return offset + nparsed; } @@ -508,7 +637,16 @@ static esp_err_t httpd_parse_req(struct httpd_data *hd) do { /* Read block into scratch buffer */ if ((blk_len = read_block(r, offset, PARSER_BLOCK_SIZE)) < 0) { - /* Return error to close socket */ + if (blk_len == HTTPD_SOCK_ERR_TIMEOUT) { + /* Retry read in case of non-fatal timeout error. + * read_block() ensures that the timeout error is + * handled properly so that this doesn't get stuck + * in an infinite loop */ + continue; + } + /* If not HTTPD_SOCK_ERR_TIMEOUT, returned error must + * be HTTPD_SOCK_ERR_FAIL which means we need to return + * failure and thereby close the underlying socket */ return ESP_FAIL; } @@ -518,8 +656,10 @@ static esp_err_t httpd_parse_req(struct httpd_data *hd) /* 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); + /* HTTP error occurred. + * Send error code as response status and + * invoke error handler */ + return httpd_req_handle_err(r, parser_data.error); } } while (parser_data.status != PARSING_COMPLETE); @@ -537,6 +677,7 @@ static void init_req(httpd_req_t *r, httpd_config_t *config) r->user_ctx = 0; r->sess_ctx = 0; r->free_ctx = 0; + r->ignore_sess_ctx_changes = 0; } static void init_req_aux(struct httpd_req_aux *ra, httpd_config_t *config) @@ -549,6 +690,9 @@ static void init_req_aux(struct httpd_req_aux *ra, httpd_config_t *config) ra->first_chunk_sent = 0; ra->req_hdrs_count = 0; ra->resp_hdrs_count = 0; +#if CONFIG_HTTPD_WS_SUPPORT + ra->ws_handshake_detect = false; +#endif memset(ra->resp_hdrs, 0, config->max_resp_headers * sizeof(struct resp_hdr)); } @@ -556,13 +700,23 @@ 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 */ + /* Check if the context has changed and needs to be cleared */ + if ((r->ignore_sess_ctx_changes == false) && (ra->sd->ctx != r->sess_ctx)) { httpd_sess_free_ctx(ra->sd->ctx, ra->sd->free_ctx); - ra->sd->ctx = r->sess_ctx; } + +#if CONFIG_HTTPD_WS_SUPPORT + /* Close the socket when a WebSocket Close request is received */ + if (ra->sd->ws_close) { + ESP_LOGD(TAG, LOG_FMT("Try closing WS connection at FD: %d"), ra->sd->fd); + httpd_sess_trigger_close(r->handle, ra->sd->fd); + } +#endif + + /* Retrieve session info from the request into the socket database. */ + ra->sd->ctx = r->sess_ctx; ra->sd->free_ctx = r->free_ctx; + ra->sd->ignore_sess_ctx_changes = r->ignore_sess_ctx_changes; /* Clear out the request and request_aux structures */ ra->sd = NULL; @@ -580,22 +734,63 @@ esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd) 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; + r->ignore_sess_ctx_changes = sd->ignore_sess_ctx_changes; + + esp_err_t ret; + +#ifdef CONFIG_HTTPD_WS_SUPPORT + /* Handle WebSocket */ + ESP_LOGD(TAG, LOG_FMT("New request, has WS? %s, sd->ws_handler valid? %s, sd->ws_close? %s"), + sd->ws_handshake_done ? "Yes" : "No", + sd->ws_handler != NULL ? "Yes" : "No", + sd->ws_close ? "Yes" : "No"); + if (sd->ws_handshake_done && sd->ws_handler != NULL) { + ret = httpd_ws_get_frame_type(r); + ESP_LOGD(TAG, LOG_FMT("New WS request from existing socket, ws_type=%d"), ra->ws_type); + + /* Stop and return here immediately if it's a CLOSE frame */ + if (ra->ws_type == HTTPD_WS_TYPE_CLOSE) { + sd->ws_close = true; + return ret; + } + + if (ra->ws_type == HTTPD_WS_TYPE_PONG) { + /* Pass the PONG frames to the handler as well, as user app might send PINGs */ + ESP_LOGD(TAG, LOG_FMT("Received PONG frame")); + } + + /* Call handler if it's a non-control frame (or if handler requests control frames, as well) */ + if (ret == ESP_OK && + (ra->ws_type < HTTPD_WS_TYPE_CLOSE || sd->ws_control_frames)) { + ret = sd->ws_handler(r); + } + + if (ret != ESP_OK) { + httpd_req_cleanup(r); + } + return ret; + } +#endif + /* Parse request */ - esp_err_t err = httpd_parse_req(hd); - if (err != ESP_OK) { + ret = httpd_parse_req(hd); + if (ret != ESP_OK) { httpd_req_cleanup(r); } - return err; + return ret; } /* Function that resets the http request data @@ -608,18 +803,25 @@ esp_err_t httpd_req_delete(struct httpd_data *hd) /* 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) { + * enough to finish off the buffers fast */ + char dummy[CONFIG_HTTPD_PURGE_BUF_LEN]; + int recv_len = MIN(sizeof(dummy), ra->remaining_len); + recv_len = httpd_req_recv(r, dummy, recv_len); + if (recv_len < 0) { httpd_req_cleanup(r); return ESP_FAIL; } - dummy[ret] = '\0'; - ESP_LOGD(TAG, LOG_FMT("purging data : %s"), dummy); + ESP_LOGD(TAG, LOG_FMT("purging data size : %d bytes"), recv_len); + +#ifdef CONFIG_HTTPD_LOG_PURGE_DATA + /* Enabling this will log discarded binary HTTP content data at + * Debug level. For large content data this may not be desirable + * as it will clutter the log */ + ESP_LOGD(TAG, "================= PURGED DATA ================="); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, dummy, recv_len, ESP_LOG_DEBUG); + ESP_LOGD(TAG, "==============================================="); +#endif } httpd_req_cleanup(r); @@ -782,9 +984,19 @@ size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *field) */ if ((val_ptr - hdr_ptr != strlen(field)) || (strncasecmp(hdr_ptr, field, strlen(field)))) { - hdr_ptr += strlen(hdr_ptr) + strlen("\r\n"); + if (count) { + /* Jump to end of header field-value string */ + hdr_ptr = 1 + strchr(hdr_ptr, '\0'); + + /* Skip all null characters (with which the line + * terminators had been overwritten) */ + while (*hdr_ptr == '\0') { + hdr_ptr++; + } + } continue; } + /* Skip ':' */ val_ptr++; @@ -828,7 +1040,16 @@ esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *r, const char *field, char *v */ if ((val_ptr - hdr_ptr != strlen(field)) || (strncasecmp(hdr_ptr, field, strlen(field)))) { - hdr_ptr += strlen(hdr_ptr) + strlen("\r\n"); + if (count) { + /* Jump to end of header field-value string */ + hdr_ptr = 1 + strchr(hdr_ptr, '\0'); + + /* Skip all null characters (with which the line + * terminators had been overwritten) */ + while (*hdr_ptr == '\0') { + hdr_ptr++; + } + } continue; } diff --git a/components/esp_http_server/src/httpd_sess.c b/components/esp_http_server/src/httpd_sess.c index 0f722211a..5a8ecfdcf 100644 --- a/components/esp_http_server/src/httpd_sess.c +++ b/components/esp_http_server/src/httpd_sess.c @@ -19,7 +19,6 @@ #include #include "esp_httpd_priv.h" -#include static const char *TAG = "httpd_sess"; @@ -78,7 +77,11 @@ esp_err_t httpd_sess_new(struct httpd_data *hd, int newfd) /* 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; + if (ret != ESP_OK) { + httpd_sess_delete(hd, hd->hd_sd[i].fd); + ESP_LOGD(TAG, LOG_FMT("open_fn failed for fd = %d"), newfd); + return ret; + } } return ESP_OK; } @@ -193,13 +196,13 @@ void httpd_sess_set_descriptors(struct httpd_data *hd, /** Check if a FD is valid */ static int fd_is_valid(int fd) { - return fcntl(fd, F_GETFD, 0) != -1 || errno != EBADF; + return fcntl(fd, F_GETFD) != -1 || errno != EBADF; } -static inline uint64_t httpd_sess_get_lru_counter() +static inline uint64_t httpd_sess_get_lru_counter(void) { static uint64_t lru_counter = 0; - return lru_counter++; + return ++lru_counter; } void httpd_sess_delete_invalid(struct httpd_data *hd) @@ -278,7 +281,9 @@ bool httpd_sess_pending(struct httpd_data *hd, int fd) 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; + if (sd->pending_fn(hd, fd) > 0) { + return true; + } } return (sd->pending_len != 0); @@ -345,6 +350,8 @@ esp_err_t httpd_sess_close_lru(struct httpd_data *hd) } } ESP_LOGD(TAG, LOG_FMT("fd = %d"), lru_fd); + struct sock_db *sd = httpd_sess_get(hd, lru_fd); + sd->lru_socket = true; return httpd_sess_trigger_close(hd, lru_fd); } @@ -375,7 +382,12 @@ static void httpd_sess_close(void *arg) { struct sock_db *sock_db = (struct sock_db *)arg; if (sock_db) { + if (sock_db->lru_counter == 0 && !sock_db->lru_socket) { + ESP_LOGD(TAG, "Skipping session close for %d as it seems to be a race condition", sock_db->fd); + return; + } int fd = sock_db->fd; + sock_db->lru_socket = false; struct httpd_data *hd = (struct httpd_data *) sock_db->handle; httpd_sess_delete(hd, fd); close(fd); diff --git a/components/esp_http_server/src/httpd_txrx.c b/components/esp_http_server/src/httpd_txrx.c index 69a5ba004..ab16f5ff8 100644 --- a/components/esp_http_server/src/httpd_txrx.c +++ b/components/esp_http_server/src/httpd_txrx.c @@ -155,9 +155,10 @@ size_t httpd_unrecv(struct httpd_req *r, const char *buf, size_t buf_len) /* 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 */ + /* Copy data into internal pending_data buffer with the exact offset + * such that it is right aligned inside the buffer */ size_t offset = sizeof(ra->sd->pending_data) - ra->sd->pending_len; - memcpy(ra->sd->pending_data + offset, buf, buf_len); + memcpy(ra->sd->pending_data + offset, buf, ra->sd->pending_len); ESP_LOGD(TAG, LOG_FMT("length = %d"), ra->sd->pending_len); return ra->sd->pending_len; } @@ -246,7 +247,9 @@ esp_err_t httpd_resp_send(httpd_req_t *r, const char *buf, ssize_t buf_len) const char *colon_separator = ": "; const char *cr_lf_seperator = "\r\n"; - if (buf_len == -1) buf_len = strlen(buf); + if (buf_len == HTTPD_RESP_USE_STRLEN) { + buf_len = strlen(buf); + } /* Request headers are no longer available */ ra->req_hdrs_count = 0; @@ -306,7 +309,9 @@ esp_err_t httpd_resp_send_chunk(httpd_req_t *r, const char *buf, ssize_t buf_len return ESP_ERR_HTTPD_INVALID_REQ; } - if (buf_len == -1) buf_len = strlen(buf); + if (buf_len == HTTPD_RESP_USE_STRLEN) { + 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"; @@ -375,78 +380,136 @@ esp_err_t httpd_resp_send_chunk(httpd_req_t *r, const char *buf, ssize_t buf_len return ESP_OK; } -esp_err_t httpd_resp_send_404(httpd_req_t *r) +esp_err_t httpd_resp_send_err(httpd_req_t *req, httpd_err_code_t error, const char *usr_msg) { - return httpd_resp_send_err(r, HTTPD_404_NOT_FOUND); -} + esp_err_t ret; + const char *msg; + const char *status; -esp_err_t httpd_resp_send_408(httpd_req_t *r) -{ - return httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT); + 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_401_UNAUTHORIZED: + status = "401 Unauthorized"; + msg = "Server known the client's identify and it must authenticate itself to get he requested response"; + break; + case HTTPD_403_FORBIDDEN: + status = "403 Forbidden"; + msg = "Server is refusing to give the requested resource to the client"; + break; + case HTTPD_404_NOT_FOUND: + status = "404 Not Found"; + msg = "This URI does not 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_500_INTERNAL_SERVER_ERROR: + default: + status = "500 Internal Server Error"; + msg = "Server has encountered an unexpected error"; + } + + /* If user has provided custom message, override default message */ + msg = usr_msg ? usr_msg : msg; + ESP_LOGW(TAG, LOG_FMT("%s - %s"), status, msg); + + /* Set error code in HTTP response */ + httpd_resp_set_status(req, status); + httpd_resp_set_type(req, HTTPD_TYPE_TEXT); + +#ifdef CONFIG_HTTPD_ERR_RESP_NO_DELAY + /* Use TCP_NODELAY option to force socket to send data in buffer + * This ensures that the error message is sent before the socket + * is closed */ + struct httpd_req_aux *ra = req->aux; + int nodelay = 1; + if (setsockopt(ra->sd->fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)) < 0) { + /* If failed to turn on TCP_NODELAY, throw warning and continue */ + ESP_LOGW(TAG, LOG_FMT("error calling setsockopt : %d"), errno); + nodelay = 0; + } +#endif + + /* Send HTTP error message */ + ret = httpd_resp_send(req, msg, HTTPD_RESP_USE_STRLEN); + +#ifdef CONFIG_HTTPD_ERR_RESP_NO_DELAY + /* If TCP_NODELAY was set successfully above, time to disable it */ + if (nodelay == 1) { + nodelay = 0; + if (setsockopt(ra->sd->fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay)) < 0) { + /* If failed to turn off TCP_NODELAY, throw error and + * return failure to signal for socket closure */ + ESP_LOGE(TAG, LOG_FMT("error calling setsockopt : %d"), errno); + return ESP_ERR_INVALID_STATE; + } + } +#endif + + return ret; } -esp_err_t httpd_resp_send_500(httpd_req_t *r) +esp_err_t httpd_register_err_handler(httpd_handle_t handle, + httpd_err_code_t error, + httpd_err_handler_func_t err_handler_fn) { - return httpd_resp_send_err(r, HTTPD_500_SERVER_ERROR); + if (handle == NULL || error >= HTTPD_ERR_CODE_MAX) { + return ESP_ERR_INVALID_ARG; + } + + struct httpd_data *hd = (struct httpd_data *) handle; + hd->err_handler_fns[error] = err_handler_fn; + return ESP_OK; } -esp_err_t httpd_resp_send_err(httpd_req_t *req, httpd_err_resp_t error) +esp_err_t httpd_req_handle_err(httpd_req_t *req, httpd_err_code_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"; + struct httpd_data *hd = (struct httpd_data *) req->handle; + esp_err_t ret; + + /* Invoke custom error handler if configured */ + if (hd->err_handler_fns[error]) { + ret = hd->err_handler_fns[error](req, error); + + /* If error code is 500, force return failure + * irrespective of the handler's return value */ + ret = (error == HTTPD_500_INTERNAL_SERVER_ERROR ? ESP_FAIL : ret); + } else { + /* If no handler is registered for this error default + * behavior is to send the HTTP error response and + * return failure for closure of underlying socket */ + httpd_resp_send_err(req, error, NULL); + ret = ESP_FAIL; } - 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)); + return ret; } int httpd_req_recv(httpd_req_t *r, char *buf, size_t buf_len) @@ -498,16 +561,9 @@ int httpd_req_to_sockfd(httpd_req_t *r) 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); + ESP_LOGW(TAG, LOG_FMT("error in %s : %d"), ctx, errno); - switch(sock_err) { + switch(errno) { case EAGAIN: case EINTR: errval = HTTPD_SOCK_ERR_TIMEOUT; @@ -551,3 +607,27 @@ int httpd_default_recv(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, } return ret; } + +int httpd_socket_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) +{ + struct sock_db *sess = httpd_sess_get(hd, sockfd); + if (!sess) { + return ESP_ERR_INVALID_ARG; + } + if (!sess->send_fn) { + return ESP_ERR_INVALID_STATE; + } + return sess->send_fn(hd, sockfd, buf, buf_len, flags); +} + +int httpd_socket_recv(httpd_handle_t hd, int sockfd, char *buf, size_t buf_len, int flags) +{ + struct sock_db *sess = httpd_sess_get(hd, sockfd); + if (!sess) { + return ESP_ERR_INVALID_ARG; + } + if (!sess->recv_fn) { + return ESP_ERR_INVALID_STATE; + } + return sess->recv_fn(hd, sockfd, buf, buf_len, flags); +} diff --git a/components/esp_http_server/src/httpd_uri.c b/components/esp_http_server/src/httpd_uri.c index e31a6108f..6d1a2389d 100644 --- a/components/esp_http_server/src/httpd_uri.c +++ b/components/esp_http_server/src/httpd_uri.c @@ -23,20 +23,112 @@ static const char *TAG = "httpd_uri"; -static int httpd_find_uri_handler(struct httpd_data *hd, - const char* uri, - httpd_method_t method) +static bool httpd_uri_match_simple(const char *uri1, const char *uri2, size_t len2) { + return strlen(uri1) == len2 && // First match lengths + (strncmp(uri1, uri2, len2) == 0); // Then match actual URIs +} + +bool httpd_uri_match_wildcard(const char *template, const char *uri, size_t len) +{ + const size_t tpl_len = strlen(template); + size_t exact_match_chars = tpl_len; + + /* Check for trailing question mark and asterisk */ + const char last = (const char) (tpl_len > 0 ? template[tpl_len - 1] : 0); + const char prevlast = (const char) (tpl_len > 1 ? template[tpl_len - 2] : 0); + const bool asterisk = last == '*' || (prevlast == '*' && last == '?'); + const bool quest = last == '?' || (prevlast == '?' && last == '*'); + + /* Minimum template string length must be: + * 0 : if neither of '*' and '?' are present + * 1 : if only '*' is present + * 2 : if only '?' is present + * 3 : if both are present + * + * The expression (asterisk + quest*2) serves as a + * case wise generator of these length values + */ + + /* abort in cases such as "?" with no preceding character (invalid template) */ + if (exact_match_chars < asterisk + quest*2) { + return false; + } + + /* account for special characters and the optional character if "?" is used */ + exact_match_chars -= asterisk + quest*2; + + if (len < exact_match_chars) { + return false; + } + + if (!quest) { + if (!asterisk && len != exact_match_chars) { + /* no special characters and different length - strncmp would return false */ + return false; + } + /* asterisk allows arbitrary trailing characters, we ignore these using + * exact_match_chars as the length limit */ + return (strncmp(template, uri, exact_match_chars) == 0); + } else { + /* question mark present */ + if (len > exact_match_chars && template[exact_match_chars] != uri[exact_match_chars]) { + /* the optional character is present, but different */ + return false; + } + if (strncmp(template, uri, exact_match_chars) != 0) { + /* the mandatory part differs */ + return false; + } + /* Now we know the URI is longer than the required part of template, + * the mandatory part matches, and if the optional character is present, it is correct. + * Match is OK if we have asterisk, i.e. any trailing characters are OK, or if + * there are no characters beyond the optional character. */ + return asterisk || len <= exact_match_chars + 1; + } +} + +/* Find handler with matching URI and method, and set + * appropriate error code if URI or method not found */ +static httpd_uri_t* httpd_find_uri_handler(struct httpd_data *hd, + const char *uri, size_t uri_len, + httpd_method_t method, + httpd_err_code_t *err) +{ + if (err) { + *err = HTTPD_404_NOT_FOUND; + } + 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; + if (!hd->hd_calls[i]) { + break; + } + ESP_LOGD(TAG, LOG_FMT("[%d] = %s"), i, hd->hd_calls[i]->uri); + + /* Check if custom URI matching function is set, + * else use simple string compare */ + if (hd->config.uri_match_fn ? + hd->config.uri_match_fn(hd->hd_calls[i]->uri, uri, uri_len) : + httpd_uri_match_simple(hd->hd_calls[i]->uri, uri, uri_len)) { + /* URIs match. Now check if method is supported */ + if (hd->hd_calls[i]->method == method) { + /* Match found! */ + if (err) { + /* Unset any error that may + * have been set earlier */ + *err = 0; + } + return hd->hd_calls[i]; + } + /* URI found but method not allowed. + * If URI is found later then this + * error must be set to 0 */ + if (err) { + *err = HTTPD_405_METHOD_NOT_ALLOWED; } } } - return -1; + return NULL; } esp_err_t httpd_register_uri_handler(httpd_handle_t handle, @@ -45,14 +137,15 @@ esp_err_t httpd_register_uri_handler(httpd_handle_t handle, 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 - */ + /* Make sure another handler with matching URI and method + * is not already registered. This will also catch cases + * when a registered URI wildcard pattern already accounts + * for the new URI being registered */ if (httpd_find_uri_handler(handle, uri_handler->uri, - uri_handler->method) != -1) { + strlen(uri_handler->uri), + uri_handler->method, NULL) != NULL) { ESP_LOGW(TAG, LOG_FMT("handler %s with method %d already registered"), uri_handler->uri, uri_handler->method); return ESP_ERR_HTTPD_HANDLER_EXISTS; @@ -78,6 +171,15 @@ esp_err_t httpd_register_uri_handler(httpd_handle_t handle, 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; +#ifdef CONFIG_HTTPD_WS_SUPPORT + hd->hd_calls[i]->is_websocket = uri_handler->is_websocket; + hd->hd_calls[i]->handle_ws_control_frames = uri_handler->handle_ws_control_frames; + if (uri_handler->supported_subprotocol) { + hd->hd_calls[i]->supported_subprotocol = strdup(uri_handler->supported_subprotocol); + } else { + hd->hd_calls[i]->supported_subprotocol = NULL; + } +#endif ESP_LOGD(TAG, LOG_FMT("[%d] installed %s"), i, uri_handler->uri); return ESP_OK; } @@ -95,15 +197,30 @@ esp_err_t httpd_unregister_uri_handler(httpd_handle_t handle, } struct httpd_data *hd = (struct httpd_data *) handle; - int i = httpd_find_uri_handler(hd, uri, method); + for (int i = 0; i < hd->config.max_uri_handlers; i++) { + if (!hd->hd_calls[i]) { + break; + } + if ((hd->hd_calls[i]->method == method) && // First match methods + (strcmp(hd->hd_calls[i]->uri, uri) == 0)) { // Then match URI string + ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, hd->hd_calls[i]->uri); - 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; - free((char*)hd->hd_calls[i]->uri); - free(hd->hd_calls[i]); - hd->hd_calls[i] = NULL; - return ESP_OK; + /* Shift the remaining non null handlers in the array + * forward by 1 so that order of insertion is maintained */ + for (i += 1; i < hd->config.max_uri_handlers; i++) { + if (!hd->hd_calls[i]) { + break; + } + hd->hd_calls[i-1] = hd->hd_calls[i]; + } + /* Nullify the following non null entry */ + hd->hd_calls[i-1] = NULL; + return ESP_OK; + } } ESP_LOGW(TAG, LOG_FMT("handler %s with method %d not found"), uri, method); return ESP_ERR_NOT_FOUND; @@ -118,17 +235,31 @@ esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char *uri) 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)) { + int i = 0, j = 0; // For keeping count of removed entries + for (; i < hd->config.max_uri_handlers; i++) { + if (!hd->hd_calls[i]) { + break; + } + if (strcmp(hd->hd_calls[i]->uri, uri) == 0) { // Match URI strings 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; + + j++; // Update count of removed entries + } else { + /* Shift the remaining non null handlers in the array + * forward by j so that order of insertion is maintained */ + hd->hd_calls[i-j] = hd->hd_calls[i]; } } + /* Nullify the following non null entries */ + for (int k = (i - j); k < i; k++) { + hd->hd_calls[k] = NULL; + } + if (!found) { ESP_LOGW(TAG, LOG_FMT("no handler found for URI %s"), uri); } @@ -138,45 +269,15 @@ esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char *uri) 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]); + if (!hd->hd_calls[i]) { + break; } - } -} + ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, hd->hd_calls[i]->uri); -/* 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; + free((char*)hd->hd_calls[i]->uri); + free(hd->hd_calls[i]); + hd->hd_calls[i] = NULL; } - return NULL; } esp_err_t httpd_uri(struct httpd_data *hd) @@ -186,15 +287,14 @@ esp_err_t httpd_uri(struct httpd_data *hd) 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; + httpd_err_code_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); + uri = httpd_find_uri_handler(hd, req->uri + res->field_data[UF_PATH].off, + res->field_data[UF_PATH].len, req->method, &err); } /* If URI with method not found, respond with error code */ @@ -202,10 +302,11 @@ esp_err_t httpd_uri(struct httpd_data *hd) 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); + return httpd_req_handle_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); + ESP_LOGW(TAG, LOG_FMT("Method '%d' not allowed for URI '%s'"), + req->method, req->uri); + return httpd_req_handle_err(req, HTTPD_405_METHOD_NOT_ALLOWED); default: return ESP_FAIL; } @@ -214,6 +315,25 @@ esp_err_t httpd_uri(struct httpd_data *hd) /* Attach user context data (passed during URI registration) into request */ req->user_ctx = uri->user_ctx; + /* Final step for a WebSocket handshake verification */ +#ifdef CONFIG_HTTPD_WS_SUPPORT + struct httpd_req_aux *aux = req->aux; + if (uri->is_websocket && aux->ws_handshake_detect && uri->method == HTTP_GET) { + ESP_LOGD(TAG, LOG_FMT("Responding WS handshake to sock %d"), aux->sd->fd); + esp_err_t ret = httpd_ws_respond_server_handshake(&hd->hd_req, uri->supported_subprotocol); + if (ret != ESP_OK) { + return ret; + } + + aux->sd->ws_handshake_done = true; + aux->sd->ws_handler = uri->handler; + aux->sd->ws_control_frames = uri->handle_ws_control_frames; + + /* Return immediately after handshake, no need to call handler here */ + return ESP_OK; + } +#endif + /* Invoke handler */ if (uri->handler(req) != ESP_OK) { /* Handler returns error, this socket should be closed */ diff --git a/components/esp_http_server/src/httpd_ws.c b/components/esp_http_server/src/httpd_ws.c new file mode 100644 index 000000000..e9e337710 --- /dev/null +++ b/components/esp_http_server/src/httpd_ws.c @@ -0,0 +1,481 @@ +// Copyright 2020 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 +#include +#include +#include +#include +#include +#include + +#include +#include "esp_httpd_priv.h" + +#ifdef CONFIG_HTTPD_WS_SUPPORT + +static const char *TAG="httpd_ws"; + +/* + * Bit masks for WebSocket frames. + * Please refer to RFC6455 Section 5.2 for more details. + */ +#define HTTPD_WS_CONTINUE 0x00U +#define HTTPD_WS_FIN_BIT 0x80U +#define HTTPD_WS_OPCODE_BITS 0x0fU +#define HTTPD_WS_MASK_BIT 0x80U +#define HTTPD_WS_LENGTH_BITS 0x7fU + +/* + * The magic GUID string used for handshake + * Please refer to RFC6455 Section 1.3 for more details. + */ +static const char ws_magic_uuid[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +/* Checks if any subprotocols from the comma seperated list matches the supported one + * + * Returns true if the response should contain a protocol field +*/ + +/** + * @brief Checks if any subprotocols from the comma seperated list matches the supported one + * + * @param supported_subprotocol[in] The subprotocol supported by the URI + * @param subprotocol[in], [in]: A comma seperate list of subprotocols requested + * @param buf_len Length of the buffer + * @return true: found a matching subprotocol + * @return false + */ +static bool httpd_ws_get_response_subprotocol(const char *supported_subprotocol, char *subprotocol, size_t buf_len) +{ + /* Request didnt contain any subprotocols */ + if (strnlen(subprotocol, buf_len) == 0) { + return false; + } + + if (supported_subprotocol == NULL) { + ESP_LOGW(TAG, "Sec-WebSocket-Protocol %s not supported, URI do not support any subprotocols", subprotocol); + return false; + } + + /* Get first subprotocol from comma seperated list */ + char *rest = NULL; + char *s = strtok_r(subprotocol, ", ", &rest); + do { + if (strncmp(s, supported_subprotocol, sizeof(subprotocol)) == 0) { + ESP_LOGD(TAG, "Requested subprotocol supported: %s", s); + return true; + } + } while ((s = strtok_r(NULL, ", ", &rest)) != NULL); + + ESP_LOGW(TAG, "Sec-WebSocket-Protocol %s not supported, supported subprotocol is %s", subprotocol, supported_subprotocol); + + /* No matches */ + return false; + +} + +esp_err_t httpd_ws_respond_server_handshake(httpd_req_t *req, const char *supported_subprotocol) +{ + /* Probe if input parameters are valid or not */ + if (!req || !req->aux) { + ESP_LOGW(TAG, LOG_FMT("Argument is invalid")); + return ESP_ERR_INVALID_ARG; + } + + /* Detect handshake - reject if handshake was ALREADY performed */ + struct httpd_req_aux *req_aux = req->aux; + if (req_aux->sd->ws_handshake_done) { + ESP_LOGW(TAG, LOG_FMT("State is invalid - Handshake has been performed")); + return ESP_ERR_INVALID_STATE; + } + + /* Detect WS version existence */ + char version_val[3] = { '\0' }; + if (httpd_req_get_hdr_value_str(req, "Sec-WebSocket-Version", version_val, sizeof(version_val)) != ESP_OK) { + ESP_LOGW(TAG, LOG_FMT("\"Sec-WebSocket-Version\" is not found")); + return ESP_ERR_NOT_FOUND; + } + + /* Detect if WS version is "13" or not. + * WS version must be 13 for now. Please refer to RFC6455 Section 4.1, Page 18 for more details. */ + if (strcasecmp(version_val, "13") != 0) { + ESP_LOGW(TAG, LOG_FMT("\"Sec-WebSocket-Version\" is not \"13\", it is: %s"), version_val); + return ESP_ERR_INVALID_VERSION; + } + + /* Grab Sec-WebSocket-Key (client key) from the header */ + /* Size of base64 coded string is equal '((input_size * 4) / 3) + (input_size / 96) + 6' including Z-term */ + char sec_key_encoded[28] = { '\0' }; + if (httpd_req_get_hdr_value_str(req, "Sec-WebSocket-Key", sec_key_encoded, sizeof(sec_key_encoded)) != ESP_OK) { + ESP_LOGW(TAG, LOG_FMT("Cannot find client key")); + return ESP_ERR_NOT_FOUND; + } + + /* Prepare server key (Sec-WebSocket-Accept), concat the string */ + char server_key_encoded[33] = { '\0' }; + uint8_t server_key_hash[20] = { 0 }; + char server_raw_text[sizeof(sec_key_encoded) + sizeof(ws_magic_uuid) + 1] = { '\0' }; + + strcpy(server_raw_text, sec_key_encoded); + strcat(server_raw_text, ws_magic_uuid); + + ESP_LOGD(TAG, LOG_FMT("Server key before encoding: %s"), server_raw_text); + + /* Generate SHA-1 first and then encode to Base64 */ + size_t key_len = strlen(server_raw_text); + mbedtls_sha1_ret((uint8_t *)server_raw_text, key_len, server_key_hash); + + size_t encoded_len = 0; + mbedtls_base64_encode((uint8_t *)server_key_encoded, sizeof(server_key_encoded), &encoded_len, + server_key_hash, sizeof(server_key_hash)); + + ESP_LOGD(TAG, LOG_FMT("Generated server key: %s"), server_key_encoded); + + char subprotocol[50] = { '\0' }; + if (httpd_req_get_hdr_value_str(req, "Sec-WebSocket-Protocol", subprotocol, sizeof(subprotocol) - 1) == ESP_ERR_HTTPD_RESULT_TRUNC) { + ESP_LOGW(TAG, "Sec-WebSocket-Protocol length exceeded buffer size of %d, was trunctated", sizeof(subprotocol)); + } + + + /* Prepare the Switching Protocol response */ + char tx_buf[192] = { '\0' }; + int fmt_len = snprintf(tx_buf, sizeof(tx_buf), + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n", server_key_encoded); + + if (fmt_len < 0 || fmt_len > sizeof(tx_buf)) { + ESP_LOGW(TAG, LOG_FMT("Failed to prepare Tx buffer")); + return ESP_FAIL; + } + + if ( httpd_ws_get_response_subprotocol(supported_subprotocol, subprotocol, sizeof(subprotocol))) { + ESP_LOGD(TAG, "subprotocol: %s", subprotocol); + int r = snprintf(tx_buf + fmt_len, sizeof(tx_buf) - fmt_len, "Sec-WebSocket-Protocol: %s\r\n", supported_subprotocol); + if (r <= 0) { + ESP_LOGE(TAG, "Error in response generation" + "(snprintf of subprotocol returned %d, buffer size: %d", r, sizeof(tx_buf)); + return ESP_FAIL; + } + + fmt_len += r; + + if (fmt_len >= sizeof(tx_buf)) { + ESP_LOGE(TAG, "Error in response generation" + "(snprintf of subprotocol returned %d, desired response len: %d, buffer size: %d", r, fmt_len, sizeof(tx_buf)); + return ESP_FAIL; + } + } + + int r = snprintf(tx_buf + fmt_len, sizeof(tx_buf) - fmt_len, "\r\n"); + if (r <= 0) { + ESP_LOGE(TAG, "Error in response generation" + "(snprintf of subprotocol returned %d, buffer size: %d", r, sizeof(tx_buf)); + return ESP_FAIL; + } + fmt_len += r; + if (fmt_len >= sizeof(tx_buf)) { + ESP_LOGE(TAG, "Error in response generation" + "(snprintf of header terminal returned %d, desired response len: %d, buffer size: %d", r, fmt_len, sizeof(tx_buf)); + return ESP_FAIL; + } + + /* Send off the response */ + if (httpd_send(req, tx_buf, fmt_len) < 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to send the response")); + return ESP_FAIL; + } + + return ESP_OK; +} + +static esp_err_t httpd_ws_check_req(httpd_req_t *req) +{ + /* Probe if input parameters are valid or not */ + if (!req || !req->aux) { + ESP_LOGW(TAG, LOG_FMT("Argument is null")); + return ESP_ERR_INVALID_ARG; + } + + /* Detect handshake - reject if handshake was NOT YET performed */ + struct httpd_req_aux *req_aux = req->aux; + if (!req_aux->sd->ws_handshake_done) { + ESP_LOGW(TAG, LOG_FMT("State is invalid - No handshake performed")); + return ESP_ERR_INVALID_STATE; + } + + return ESP_OK; +} + +static esp_err_t httpd_ws_unmask_payload(uint8_t *payload, size_t len, const uint8_t *mask_key) +{ + if (len < 1 || !payload) { + ESP_LOGW(TAG, LOG_FMT("Invalid payload provided")); + return ESP_ERR_INVALID_ARG; + } + + for (size_t idx = 0; idx < len; idx++) { + payload[idx] = (payload[idx] ^ mask_key[idx % 4]); + } + + return ESP_OK; +} + +esp_err_t httpd_ws_recv_frame(httpd_req_t *req, httpd_ws_frame_t *frame, size_t max_len) +{ + esp_err_t ret = httpd_ws_check_req(req); + if (ret != ESP_OK) { + return ret; + } + + struct httpd_req_aux *aux = req->aux; + if (aux == NULL) { + ESP_LOGW(TAG, LOG_FMT("Invalid Aux pointer")); + return ESP_ERR_INVALID_ARG; + } + + if (!frame) { + ESP_LOGW(TAG, LOG_FMT("Frame pointer is invalid")); + return ESP_ERR_INVALID_ARG; + } + + /* Assign the frame info from the previous reading */ + frame->type = aux->ws_type; + frame->final = aux->ws_final; + + /* Grab the second byte */ + uint8_t second_byte = 0; + if (httpd_recv_with_opt(req, (char *)&second_byte, sizeof(second_byte), false) <= 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to receive the second byte")); + return ESP_FAIL; + } + + /* Parse the second byte */ + /* Please refer to RFC6455 Section 5.2 for more details */ + bool masked = (second_byte & HTTPD_WS_MASK_BIT) != 0; + + /* Interpret length */ + uint8_t init_len = second_byte & HTTPD_WS_LENGTH_BITS; + if (init_len < 126) { + /* Case 1: If length is 0-125, then this length bit is 7 bits */ + frame->len = init_len; + } else if (init_len == 126) { + /* Case 2: If length byte is 126, then this frame's length bit is 16 bits */ + uint8_t length_bytes[2] = { 0 }; + if (httpd_recv_with_opt(req, (char *)length_bytes, sizeof(length_bytes), false) <= 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to receive 2 bytes length")); + return ESP_FAIL; + } + + frame->len = ((uint32_t)(length_bytes[0] << 8U) | (length_bytes[1])); + } else if (init_len == 127) { + /* Case 3: If length is byte 127, then this frame's length bit is 64 bits */ + uint8_t length_bytes[8] = { 0 }; + if (httpd_recv_with_opt(req, (char *)length_bytes, sizeof(length_bytes), false) <= 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to receive 2 bytes length")); + return ESP_FAIL; + } + + frame->len = (((uint64_t)length_bytes[0] << 56U) | + ((uint64_t)length_bytes[1] << 48U) | + ((uint64_t)length_bytes[2] << 40U) | + ((uint64_t)length_bytes[3] << 32U) | + ((uint64_t)length_bytes[4] << 24U) | + ((uint64_t)length_bytes[5] << 16U) | + ((uint64_t)length_bytes[6] << 8U) | + ((uint64_t)length_bytes[7])); + } + + /* We only accept the incoming packet length that is smaller than the max_len (or it will overflow the buffer!) */ + if (frame->len > max_len) { + ESP_LOGW(TAG, LOG_FMT("WS Message too long")); + return ESP_ERR_INVALID_SIZE; + } + + /* If this frame is masked, dump the mask as well */ + uint8_t mask_key[4] = { 0 }; + if (masked) { + if (httpd_recv_with_opt(req, (char *)mask_key, sizeof(mask_key), false) <= 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to receive mask key")); + return ESP_FAIL; + } + } else { + /* If the WS frame from client to server is not masked, it should be rejected. + * Please refer to RFC6455 Section 5.2 for more details. */ + ESP_LOGW(TAG, LOG_FMT("WS frame is not properly masked.")); + return ESP_ERR_INVALID_STATE; + } + + /* Receive buffer */ + /* If there's nothing to receive, return and stop here. */ + if (frame->len == 0) { + return ESP_OK; + } + + if (frame->payload == NULL) { + ESP_LOGW(TAG, LOG_FMT("Payload buffer is null")); + return ESP_FAIL; + } + + if (httpd_recv_with_opt(req, (char *)frame->payload, frame->len, false) <= 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to receive payload")); + return ESP_FAIL; + } + + /* Unmask payload */ + httpd_ws_unmask_payload(frame->payload, frame->len, mask_key); + + return ESP_OK; +} + +esp_err_t httpd_ws_send_frame(httpd_req_t *req, httpd_ws_frame_t *frame) +{ + esp_err_t ret = httpd_ws_check_req(req); + if (ret != ESP_OK) { + return ret; + } + return httpd_ws_send_frame_async(req->handle, httpd_req_to_sockfd(req), frame); +} + +esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t *frame) +{ + if (!frame) { + ESP_LOGW(TAG, LOG_FMT("Argument is invalid")); + return ESP_ERR_INVALID_ARG; + } + + /* Prepare Tx buffer - maximum length is 14, which includes 2 bytes header, 8 bytes length, 4 bytes mask key */ + uint8_t tx_len = 0; + uint8_t header_buf[10] = {0 }; + /* Set the `FIN` bit by default if message is not fragmented. Else, set it as per the `final` field */ + header_buf[0] |= (!frame->fragmented) ? HTTPD_WS_FIN_BIT : (frame->final? HTTPD_WS_FIN_BIT: HTTPD_WS_CONTINUE); + header_buf[0] |= frame->type; /* Type (opcode): 4 bits */ + + if (frame->len <= 125) { + header_buf[1] = frame->len & 0x7fU; /* Length for 7 bits */ + tx_len = 2; + } else if (frame->len > 125 && frame->len < UINT16_MAX) { + header_buf[1] = 126; /* Length for 16 bits */ + header_buf[2] = (frame->len >> 8U) & 0xffU; + header_buf[3] = frame->len & 0xffU; + tx_len = 4; + } else { + header_buf[1] = 127; /* Length for 64 bits */ + uint8_t shift_idx = sizeof(uint64_t) - 1; /* Shift index starts at 7 */ + uint64_t len64 = frame->len; /* Raise variable size to make sure we won't shift by more bits + * than the length has (to avoid undefined behaviour) */ + for (int8_t idx = 2; idx <= 9; idx++) { + /* Now do shifting (be careful of endianness, i.e. when buffer index is 2, frame length shift index is 7) */ + header_buf[idx] = (len64 >> (shift_idx * 8)) & 0xffU; + shift_idx--; + } + tx_len = 10; + } + + /* WebSocket server does not required to mask response payload, so leave the MASK bit as 0. */ + header_buf[1] &= (~HTTPD_WS_MASK_BIT); + + struct sock_db *sess = httpd_sess_get(hd, fd); + if (!sess) { + return ESP_ERR_INVALID_ARG; + } + + /* Send off header */ + if (sess->send_fn(hd, fd, (const char *)header_buf, tx_len, 0) < 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to send WS header")); + return ESP_FAIL; + } + + /* Send off payload */ + if(frame->len > 0 && frame->payload != NULL) { + if (sess->send_fn(hd, fd, (const char *)frame->payload, frame->len, 0) < 0) { + ESP_LOGW(TAG, LOG_FMT("Failed to send WS payload")); + return ESP_FAIL; + } + } + + return ESP_OK; +} + +esp_err_t httpd_ws_get_frame_type(httpd_req_t *req) +{ + esp_err_t ret = httpd_ws_check_req(req); + if (ret != ESP_OK) { + return ret; + } + + struct httpd_req_aux *aux = req->aux; + if (aux == NULL) { + ESP_LOGW(TAG, LOG_FMT("Invalid Aux pointer")); + return ESP_ERR_INVALID_ARG; + } + + /* Read the first byte from the frame to get the FIN flag and Opcode */ + /* Please refer to RFC6455 Section 5.2 for more details */ + uint8_t first_byte = 0; + if (httpd_recv_with_opt(req, (char *)&first_byte, sizeof(first_byte), false) <= 0) { + /* If the recv() return code is <= 0, then this socket FD is invalid (i.e. a broken connection) */ + /* Here we mark it as a Close message and close it later. */ + ESP_LOGW(TAG, LOG_FMT("Failed to read header byte (socket FD invalid), closing socket now")); + aux->ws_final = true; + aux->ws_type = HTTPD_WS_TYPE_CLOSE; + return ESP_OK; + } + + ESP_LOGD(TAG, LOG_FMT("First byte received: 0x%02X"), first_byte); + + /* Decode the FIN flag and Opcode from the byte */ + aux->ws_final = (first_byte & HTTPD_WS_FIN_BIT) != 0; + aux->ws_type = (first_byte & HTTPD_WS_OPCODE_BITS); + + /* Reply to PING. For PONG and CLOSE, it will be handled elsewhere. */ + if(aux->ws_type == HTTPD_WS_TYPE_PING) { + ESP_LOGD(TAG, LOG_FMT("Got a WS PING frame, Replying PONG...")); + + /* Read the rest of the PING frame, for PONG to reply back. */ + /* Please refer to RFC6455 Section 5.5.2 for more details */ + httpd_ws_frame_t frame; + uint8_t frame_buf[128] = { 0 }; + memset(&frame, 0, sizeof(httpd_ws_frame_t)); + frame.payload = frame_buf; + + if(httpd_ws_recv_frame(req, &frame, 126) != ESP_OK) { + ESP_LOGD(TAG, LOG_FMT("Cannot receive the full PING frame")); + return ESP_ERR_INVALID_STATE; + } + + /* Now turn the frame to PONG */ + frame.type = HTTPD_WS_TYPE_PONG; + return httpd_ws_send_frame(req, &frame); + } + + return ESP_OK; +} + +httpd_ws_client_info_t httpd_ws_get_fd_info(httpd_handle_t hd, int fd) +{ + struct sock_db *sess = httpd_sess_get(hd, fd); + + if (sess == NULL) { + return HTTPD_WS_CLIENT_INVALID; + } + bool is_active_ws = sess->ws_handshake_done && (!sess->ws_close); + return is_active_ws ? HTTPD_WS_CLIENT_WEBSOCKET : HTTPD_WS_CLIENT_HTTP; +} + +#endif /* CONFIG_HTTPD_WS_SUPPORT */ diff --git a/components/esp_http_server/test/CMakeLists.txt b/components/esp_http_server/test/CMakeLists.txt new file mode 100644 index 000000000..8f258b833 --- /dev/null +++ b/components/esp_http_server/test/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRC_DIRS "." + PRIV_INCLUDE_DIRS "." + PRIV_REQUIRES cmock test_utils esp_http_server) diff --git a/components/esp_http_server/test/component.mk b/components/esp_http_server/test/component.mk new file mode 100644 index 000000000..ce464a212 --- /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 000000000..186df6c9b --- /dev/null +++ b/components/esp_http_server/test/test_http_server.c @@ -0,0 +1,253 @@ +// 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 +#include +#include +#include + +#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); +} + +TEST_CASE("URI Wildcard Matcher Tests", "[HTTP SERVER]") +{ + struct uritest { + const char *template; + const char *uri; + bool matches; + }; + + struct uritest uris[] = { + {"/", "/", true}, + {"", "", true}, + {"/", "", false}, + {"/wrong", "/", false}, + {"/", "/wrong", false}, + {"/asdfghjkl/qwertrtyyuiuioo", "/asdfghjkl/qwertrtyyuiuioo", true}, + {"/path", "/path", true}, + {"/path", "/path/", false}, + {"/path/", "/path", false}, + + {"?", "", false}, // this is not valid, but should not crash + {"?", "sfsdf", false}, + + {"/path/?", "/pa", false}, + {"/path/?", "/path", true}, + {"/path/?", "/path/", true}, + {"/path/?", "/path/alalal", false}, + + {"/path/*", "/path", false}, + {"/path/*", "/", false}, + {"/path/*", "/path/", true}, + {"/path/*", "/path/blabla", true}, + + {"*", "", true}, + {"*", "/", true}, + {"*", "/aaa", true}, + + {"/path/?*", "/pat", false}, + {"/path/?*", "/pathb", false}, + {"/path/?*", "/pathxx", false}, + {"/path/?*", "/pathblabla", false}, + {"/path/?*", "/path", true}, + {"/path/?*", "/path/", true}, + {"/path/?*", "/path/blabla", true}, + + {"/path/*?", "/pat", false}, + {"/path/*?", "/pathb", false}, + {"/path/*?", "/pathxx", false}, + {"/path/*?", "/path", true}, + {"/path/*?", "/path/", true}, + {"/path/*?", "/path/blabla", true}, + + {"/path/*/xxx", "/path/", false}, + {"/path/*/xxx", "/path/*/xxx", true}, + {} + }; + + struct uritest *ut = &uris[0]; + + while(ut->template != 0) { + bool match = httpd_uri_match_wildcard(ut->template, ut->uri, strlen(ut->uri)); + TEST_ASSERT(match == ut->matches); + ut++; + } +} + +TEST_CASE("Max Allowed Sockets Test", "[HTTP SERVER]") +{ + test_case_uses_tcpip(); + + httpd_handle_t hd; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + /* Starting server with default config options should pass */ + TEST_ASSERT(httpd_start(&hd, &config) == ESP_OK); + TEST_ASSERT(httpd_stop(hd) == ESP_OK); + + /* Default value of max_open_sockets is already set as per + * maximum limit imposed by LWIP. Increasing this beyond the + * maximum allowed value, without increasing LWIP limit, + * should fail */ + config.max_open_sockets += 1; + TEST_ASSERT(httpd_start(&hd, &config) != ESP_OK); +}