diff --git a/components/esp_http_server/Kconfig b/components/esp_http_server/Kconfig index c8e4cc115..018289515 100644 --- a/components/esp_http_server/Kconfig +++ b/components/esp_http_server/Kconfig @@ -13,4 +13,11 @@ menu "HTTP Server" 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 + endmenu diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index 94dbb30c3..e02aa75f4 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -471,6 +471,122 @@ 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, + + /* 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 @@ -901,7 +1017,7 @@ esp_err_t httpd_resp_send_chunk(httpd_req_t *r, const char *buf, ssize_t buf_len * - ESP_ERR_HTTPD_RESP_SEND : Error in raw send * - ESP_ERR_HTTPD_INVALID_REQ : Invalid request */ -inline esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *str) { +static inline esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *str) { return httpd_resp_send(r, str, (str == NULL) ? 0 : strlen(str)); } @@ -922,7 +1038,7 @@ inline esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *str) { * - ESP_ERR_HTTPD_RESP_SEND : Error in raw send * - ESP_ERR_HTTPD_INVALID_REQ : Invalid request */ -inline esp_err_t httpd_resp_sendstr_chunk(httpd_req_t *r, const char *str) { +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 : strlen(str)); } @@ -1014,6 +1130,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 * @@ -1035,7 +1175,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 @@ -1058,7 +1200,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 @@ -1081,7 +1225,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 diff --git a/components/esp_http_server/src/esp_httpd_priv.h b/components/esp_http_server/src/esp_httpd_priv.h index 68bc9d64d..44a546009 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,64 +54,6 @@ 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. */ @@ -131,7 +73,7 @@ struct sock_db { }; /** - * @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 { @@ -151,7 +93,7 @@ struct httpd_req_aux { }; /** - * @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 +101,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 +149,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 +171,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 +194,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 +233,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 +288,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 +298,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 +308,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 +354,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 +377,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 +403,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 diff --git a/components/esp_http_server/src/httpd_main.c b/components/esp_http_server/src/httpd_main.c index dc4cbd86e..52228a97c 100644 --- a/components/esp_http_server/src/httpd_main.c +++ b/components/esp_http_server/src/httpd_main.c @@ -288,31 +288,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; } @@ -320,6 +332,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); diff --git a/components/esp_http_server/src/httpd_parse.c b/components/esp_http_server/src/httpd_parse.c index 51843182d..aa40809f1 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; } @@ -194,6 +194,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,6 +210,7 @@ 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; } @@ -221,6 +225,7 @@ static esp_err_t cb_header_field(http_parser *parser, const char *at, size_t len parser_data->status = PARSING_HDR_FIELD; } 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; } @@ -251,6 +256,7 @@ static esp_err_t cb_header_value(http_parser *parser, const char *at, size_t len ra->req_hdrs_count++; } 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,6 +281,9 @@ 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; } @@ -287,6 +296,7 @@ static esp_err_t cb_headers_complete(http_parser *parser) parser_data->last.at += parser_data->last.length; } 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; } @@ -300,7 +310,9 @@ static esp_err_t cb_headers_complete(http_parser *parser) if (parser->upgrade) { ESP_LOGW(TAG, LOG_FMT("upgrade from HTTP not supported")); - parser_data->error = HTTPD_XXX_UPGRADE_NOT_SUPPORTED; + /* There is no specific HTTP error code to notify the client that + * upgrade is not supported, thus sending 400 Bad Request */ + parser_data->error = HTTPD_400_BAD_REQUEST; parser_data->status = PARSING_FAILED; return ESP_FAIL; } @@ -320,6 +332,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 +342,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; } @@ -352,11 +366,15 @@ static esp_err_t cb_no_body(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_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; } @@ -369,6 +387,7 @@ static esp_err_t cb_no_body(http_parser *parser) * 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; } @@ -396,13 +415,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 +448,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 +460,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,6 +486,8 @@ 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) { @@ -457,8 +497,8 @@ static int parse_block(http_parser *parser, size_t offset, size_t length) 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; @@ -508,7 +548,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 +567,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); diff --git a/components/esp_http_server/src/httpd_txrx.c b/components/esp_http_server/src/httpd_txrx.c index 7721b11b7..9de9e581f 100644 --- a/components/esp_http_server/src/httpd_txrx.c +++ b/components/esp_http_server/src/httpd_txrx.c @@ -379,78 +379,128 @@ 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) -{ - return httpd_resp_send_err(r, HTTPD_404_NOT_FOUND); -} - -esp_err_t httpd_resp_send_408(httpd_req_t *r) -{ - return httpd_resp_send_err(r, HTTPD_408_REQ_TIMEOUT); -} - -esp_err_t httpd_resp_send_500(httpd_req_t *r) -{ - return httpd_resp_send_err(r, HTTPD_500_SERVER_ERROR); -} - -esp_err_t httpd_resp_send_err(httpd_req_t *req, httpd_err_resp_t error) +esp_err_t httpd_resp_send_err(httpd_req_t *req, httpd_err_code_t error, const char *usr_msg) { + esp_err_t ret; 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"; + 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 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); - httpd_resp_set_status (req, status); - httpd_resp_set_type (req, HTTPD_TYPE_TEXT); - return httpd_resp_send (req, msg, strlen(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, strlen(msg)); + +#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_register_err_handler(httpd_handle_t handle, + httpd_err_code_t error, + httpd_err_handler_func_t err_handler_fn) +{ + 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_req_handle_err(httpd_req_t *req, httpd_err_code_t 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; + } + return ret; } int httpd_req_recv(httpd_req_t *r, char *buf, size_t buf_len) diff --git a/components/esp_http_server/src/httpd_uri.c b/components/esp_http_server/src/httpd_uri.c index 4a9841f02..9ffd3f914 100644 --- a/components/esp_http_server/src/httpd_uri.c +++ b/components/esp_http_server/src/httpd_uri.c @@ -93,7 +93,7 @@ bool httpd_uri_match_wildcard(const char *template, const char *uri, size_t len) 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_resp_t *err) + httpd_err_code_t *err) { if (err) { *err = HTTPD_404_NOT_FOUND; @@ -279,7 +279,7 @@ 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); @@ -294,11 +294,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); + return httpd_req_handle_err(req, HTTPD_405_METHOD_NOT_ALLOWED); default: return ESP_FAIL; }