diff --git a/components/esp_http_server/Kconfig b/components/esp_http_server/Kconfig index c8e4cc115d..0182895153 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 94dbb30c37..e02aa75f4c 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 68bc9d64de..44a546009e 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 dc4cbd86ea..52228a97c4 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 51843182dd..aa40809f1e 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 7721b11b79..9de9e581f1 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 4a9841f02a..9ffd3f9147 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; } diff --git a/components/esp_https_server/include/esp_https_server.h b/components/esp_https_server/include/esp_https_server.h index 19c326ed51..e69a5a294e 100644 --- a/components/esp_https_server/include/esp_https_server.h +++ b/components/esp_https_server/include/esp_https_server.h @@ -95,6 +95,7 @@ typedef struct httpd_ssl_config httpd_ssl_config_t; .global_transport_ctx_free_fn = NULL, \ .open_fn = NULL, \ .close_fn = NULL, \ + .uri_match_fn = NULL \ }, \ .cacert_pem = NULL, \ .cacert_len = 0, \ diff --git a/examples/protocols/http_server/advanced_tests/main/tests.c b/examples/protocols/http_server/advanced_tests/main/tests.c index 8845585bee..589bf96469 100644 --- a/examples/protocols/http_server/advanced_tests/main/tests.c +++ b/examples/protocols/http_server/advanced_tests/main/tests.c @@ -55,6 +55,7 @@ esp_err_t echo_post_handler(httpd_req_t *req) int ret; if (!buf) { + ESP_LOGE(TAG, "Failed to allocate memory of %d bytes!", req->content_len + 1); httpd_resp_send_500(req); return ESP_FAIL; } @@ -84,12 +85,15 @@ esp_err_t echo_post_handler(httpd_req_t *req) if (hdr_len) { /* Read Custom header value */ req_hdr = malloc(hdr_len + 1); - if (req_hdr) { - httpd_req_get_hdr_value_str(req, "Custom", req_hdr, hdr_len + 1); - - /* Set as additional header for response packet */ - httpd_resp_set_hdr(req, "Custom", req_hdr); + if (!req_hdr) { + ESP_LOGE(TAG, "Failed to allocate memory of %d bytes!", hdr_len + 1); + httpd_resp_send_500(req); + return ESP_FAIL; } + httpd_req_get_hdr_value_str(req, "Custom", req_hdr, hdr_len + 1); + + /* Set as additional header for response packet */ + httpd_resp_set_hdr(req, "Custom", req_hdr); } httpd_resp_send(req, buf, req->content_len); free (req_hdr); diff --git a/examples/protocols/http_server/advanced_tests/scripts/test.py b/examples/protocols/http_server/advanced_tests/scripts/test.py index 14371abd80..ceddc19341 100644 --- a/examples/protocols/http_server/advanced_tests/scripts/test.py +++ b/examples/protocols/http_server/advanced_tests/scripts/test.py @@ -367,7 +367,7 @@ def put_hello(dut, port): def post_hello(dut, port): # POST /hello returns 405' - Utility.console_log("[test] POST /hello returns 404 =>", end=' ') + Utility.console_log("[test] POST /hello returns 405 =>", end=' ') conn = http.client.HTTPConnection(dut, int(port), timeout=15) conn.request("POST", "/hello", "Hello") resp = conn.getresponse() @@ -541,8 +541,10 @@ def leftover_data_test(dut, port): if not test_val("False URI Status", str(404), str(resp.status)): s.close() return False - resp.read() + # socket would have been closed by server due to error + s.close() + s = http.client.HTTPConnection(dut + ":" + port, timeout=15) s.request("GET", url='/hello') resp = s.getresponse() if not test_val("Hello World Data", "Hello World!", resp.read().decode()): @@ -637,7 +639,7 @@ def code_500_server_error_test(dut, port): Utility.console_log("[test] 500 Server Error test =>", end=' ') s = Session(dut, port) # Sending a very large content length will cause malloc to fail - content_len = 2**31 + content_len = 2**30 s.client.sendall(("POST /echo HTTP/1.1\r\nHost: " + dut + "\r\nContent-Length: " + str(content_len) + "\r\n\r\nABCD").encode()) s.read_resp_hdrs() s.read_resp_data() @@ -802,7 +804,7 @@ def send_postx_hdr_len(dut, port, length): hdr = s.read_resp_hdrs() resp = s.read_resp_data() s.close() - if "Custom" in hdr: + if hdr and ("Custom" in hdr): return (hdr["Custom"] == custom_hdr_val), resp return False, s.status @@ -826,7 +828,7 @@ def test_upgrade_not_supported(dut, port): s.client.sendall(("OPTIONS * HTTP/1.1\r\nHost:" + dut + "\r\nUpgrade: TLS/1.0\r\nConnection: Upgrade\r\n\r\n").encode()) s.read_resp_hdrs() s.read_resp_data() - if not test_val("Client Error", "200", s.status): + if not test_val("Client Error", "400", s.status): s.close() return False s.close() diff --git a/examples/protocols/http_server/file_serving/main/file_server.c b/examples/protocols/http_server/file_serving/main/file_server.c index 2ca428f9df..9d301b45bd 100644 --- a/examples/protocols/http_server/file_serving/main/file_server.c +++ b/examples/protocols/http_server/file_serving/main/file_server.c @@ -72,9 +72,10 @@ static esp_err_t http_resp_dir_html(httpd_req_t *req) const size_t entrypath_offset = strlen(fullpath); if (!dir) { - /* If opening directory failed then send 404 server error */ - httpd_resp_send_404(req); - return ESP_OK; + ESP_LOGE(TAG, "Failed to stat dir : %s", fullpath); + /* Respond with 404 Not Found */ + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Directory does not exist"); + return ESP_FAIL; } /* Send HTML file header */ @@ -172,18 +173,17 @@ static esp_err_t http_resp_file(httpd_req_t *req) strcat(filepath, req->uri); if (stat(filepath, &file_stat) == -1) { ESP_LOGE(TAG, "Failed to stat file : %s", filepath); - /* If file doesn't exist respond with 404 Not Found */ - httpd_resp_send_404(req); - return ESP_OK; + /* Respond with 404 Not Found */ + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist"); + return ESP_FAIL; } fd = fopen(filepath, "r"); if (!fd) { ESP_LOGE(TAG, "Failed to read existing file : %s", filepath); - /* If file exists but unable to open respond with 500 Server Error */ - httpd_resp_set_status(req, "500 Server Error"); - httpd_resp_sendstr(req, "Failed to read existing file!"); - return ESP_OK; + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file"); + return ESP_FAIL; } ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filepath, file_stat.st_size); @@ -202,10 +202,9 @@ static esp_err_t http_resp_file(httpd_req_t *req) ESP_LOGE(TAG, "File sending failed!"); /* Abort sending file */ httpd_resp_sendstr_chunk(req, NULL); - /* Send error message with status code */ - httpd_resp_set_status(req, "500 Server Error"); - httpd_resp_sendstr(req, "Failed to send file!"); - return ESP_OK; + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file"); + return ESP_FAIL; } /* Keep looping till the whole file is sent */ @@ -249,10 +248,8 @@ static esp_err_t upload_post_handler(httpd_req_t *req) if (strlen(filename) == 0 || filename[strlen(filename) - 1] == '/') { ESP_LOGE(TAG, "Invalid file name : %s", filename); /* Respond with 400 Bad Request */ - httpd_resp_set_status(req, "400 Bad Request"); - /* Send failure reason */ - httpd_resp_sendstr(req, "Invalid file name!"); - return ESP_OK; + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid file name"); + return ESP_FAIL; } /* Retrieve the base path of file storage to construct the full path */ @@ -262,18 +259,18 @@ static esp_err_t upload_post_handler(httpd_req_t *req) strcat(filepath, filename); if (stat(filepath, &file_stat) == 0) { ESP_LOGE(TAG, "File already exists : %s", filepath); - /* If file exists respond with 400 Bad Request */ - httpd_resp_set_status(req, "400 Bad Request"); - httpd_resp_sendstr(req, "File already exists!"); - return ESP_OK; + /* Respond with 400 Bad Request */ + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File already exists"); + return ESP_FAIL; } /* File cannot be larger than a limit */ if (req->content_len > MAX_FILE_SIZE) { ESP_LOGE(TAG, "File too large : %d bytes", req->content_len); - httpd_resp_set_status(req, "400 Bad Request"); - httpd_resp_sendstr(req, "File size must be less than " - MAX_FILE_SIZE_STR "!"); + /* Respond with 400 Bad Request */ + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, + "File size must be less than " + MAX_FILE_SIZE_STR "!"); /* Return failure to close underlying connection else the * incoming file content will keep the socket busy */ return ESP_FAIL; @@ -282,10 +279,9 @@ static esp_err_t upload_post_handler(httpd_req_t *req) fd = fopen(filepath, "w"); if (!fd) { ESP_LOGE(TAG, "Failed to create file : %s", filepath); - /* If file creation failed, respond with 500 Server Error */ - httpd_resp_set_status(req, "500 Server Error"); - httpd_resp_sendstr(req, "Failed to create file!"); - return ESP_OK; + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to create file"); + return ESP_FAIL; } ESP_LOGI(TAG, "Receiving file : %s...", filename); @@ -314,10 +310,9 @@ static esp_err_t upload_post_handler(httpd_req_t *req) unlink(filepath); ESP_LOGE(TAG, "File reception failed!"); - /* Return failure reason with status code */ - httpd_resp_set_status(req, "500 Server Error"); - httpd_resp_sendstr(req, "Failed to receive file!"); - return ESP_OK; + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to receive file"); + return ESP_FAIL; } /* Write buffer content to file on storage */ @@ -328,9 +323,9 @@ static esp_err_t upload_post_handler(httpd_req_t *req) unlink(filepath); ESP_LOGE(TAG, "File write failed!"); - httpd_resp_set_status(req, "500 Server Error"); - httpd_resp_sendstr(req, "Failed to write file to storage!"); - return ESP_OK; + /* Respond with 500 Internal Server Error */ + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to write file to storage"); + return ESP_FAIL; } /* Keep track of remaining size of @@ -363,10 +358,8 @@ static esp_err_t delete_post_handler(httpd_req_t *req) if (strlen(filename) == 0 || filename[strlen(filename) - 1] == '/') { ESP_LOGE(TAG, "Invalid file name : %s", filename); /* Respond with 400 Bad Request */ - httpd_resp_set_status(req, "400 Bad Request"); - /* Send failure reason */ - httpd_resp_sendstr(req, "Invalid file name!"); - return ESP_OK; + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid file name"); + return ESP_FAIL; } /* Retrieve the base path of file storage to construct the full path */ @@ -376,10 +369,9 @@ static esp_err_t delete_post_handler(httpd_req_t *req) strcat(filepath, filename); if (stat(filepath, &file_stat) == -1) { ESP_LOGE(TAG, "File does not exist : %s", filename); - /* If file does not exist respond with 400 Bad Request */ - httpd_resp_set_status(req, "400 Bad Request"); - httpd_resp_sendstr(req, "File does not exist!"); - return ESP_OK; + /* Respond with 400 Bad Request */ + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "File does not exist"); + return ESP_FAIL; } ESP_LOGI(TAG, "Deleting file : %s", filename); diff --git a/examples/protocols/http_server/simple/main/main.c b/examples/protocols/http_server/simple/main/main.c index d4b7b3869e..9ada49cabc 100644 --- a/examples/protocols/http_server/simple/main/main.c +++ b/examples/protocols/http_server/simple/main/main.c @@ -152,6 +152,33 @@ httpd_uri_t echo = { .user_ctx = NULL }; +/* This handler allows the custom error handling functionality to be + * tested from client side. For that, when a PUT request 0 is sent to + * URI /ctrl, the /hello and /echo URIs are unregistered and following + * custom error handler http_404_error_handler() is registered. + * Afterwards, when /hello or /echo is requested, this custom error + * handler is invoked which, after sending an error message to client, + * either closes the underlying socket (when requested URI is /echo) + * or keeps it open (when requested URI is /hello). This allows the + * client to infer if the custom error handler is functioning as expected + * by observing the socket state. + */ +esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err) +{ + if (strcmp("/hello", req->uri) == 0) { + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "/hello URI is not available"); + /* Return ESP_OK to keep underlying socket open */ + return ESP_OK; + } else if (strcmp("/echo", req->uri) == 0) { + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "/echo URI is not available"); + /* Return ESP_FAIL to close underlying socket */ + return ESP_FAIL; + } + /* For any other URI send 404 and close socket */ + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Some 404 error message"); + return ESP_FAIL; +} + /* An HTTP PUT handler. This demonstrates realtime * registration and deregistration of URI handlers */ @@ -168,15 +195,19 @@ esp_err_t ctrl_put_handler(httpd_req_t *req) } if (buf == '0') { - /* Handler can be unregistered using the uri string */ + /* URI handlers can be unregistered using the uri string */ ESP_LOGI(TAG, "Unregistering /hello and /echo URIs"); httpd_unregister_uri(req->handle, "/hello"); httpd_unregister_uri(req->handle, "/echo"); + /* Register the custom error handler */ + httpd_register_err_handler(req->handle, HTTPD_404_NOT_FOUND, http_404_error_handler); } else { ESP_LOGI(TAG, "Registering /hello and /echo URIs"); httpd_register_uri_handler(req->handle, &hello); httpd_register_uri_handler(req->handle, &echo); + /* Unregister custom error handler */ + httpd_register_err_handler(req->handle, HTTPD_404_NOT_FOUND, NULL); } /* Respond with empty body */ diff --git a/examples/protocols/http_server/simple/scripts/client.py b/examples/protocols/http_server/simple/scripts/client.py index b43af08014..5e070b98b8 100644 --- a/examples/protocols/http_server/simple/scripts/client.py +++ b/examples/protocols/http_server/simple/scripts/client.py @@ -19,7 +19,20 @@ from __future__ import unicode_literals from builtins import str import http.client import argparse -import Utility + +try: + import Utility +except ImportError: + import sys + import os + + # This environment variable is expected on the host machine + # > export TEST_FW_PATH=~/esp/esp-idf/tools/tiny-test-fw + test_fw_path = os.getenv("TEST_FW_PATH") + if test_fw_path and test_fw_path not in sys.path: + sys.path.insert(0, test_fw_path) + + import Utility def verbose_print(verbosity, *args): @@ -27,6 +40,16 @@ def verbose_print(verbosity, *args): Utility.console_log(''.join(str(elems) for elems in args)) +def test_val(text, expected, received): + if expected != received: + Utility.console_log(" Fail!") + Utility.console_log(" [reason] " + text + ":") + Utility.console_log(" expected: " + str(expected)) + Utility.console_log(" received: " + str(received)) + return False + return True + + def test_get_handler(ip, port, verbosity=False): verbose_print(verbosity, "======== GET HANDLER TEST =============") # Establish HTTP connection @@ -44,12 +67,15 @@ def test_get_handler(ip, port, verbosity=False): resp = sess.getresponse() resp_hdrs = resp.getheaders() resp_data = resp.read().decode() - try: - if resp.getheader("Custom-Header-1") != "Custom-Value-1": - return False - if resp.getheader("Custom-Header-2") != "Custom-Value-2": - return False - except Exception: + # Close HTTP connection + sess.close() + + if not ( + test_val("Status code mismatch", 200, resp.status) and + test_val("Response mismatch", "Custom-Value-1", resp.getheader("Custom-Header-1")) and + test_val("Response mismatch", "Custom-Value-2", resp.getheader("Custom-Header-2")) and + test_val("Response mismatch", "Hello World!", resp_data) + ): return False verbose_print(verbosity, "vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv") @@ -59,10 +85,7 @@ def test_get_handler(ip, port, verbosity=False): verbose_print(verbosity, "\t", k, ": ", v) verbose_print(verbosity, "Response Data : " + resp_data) verbose_print(verbosity, "========================================\n") - - # Close HTTP connection - sess.close() - return (resp_data == "Hello World!") + return True def test_post_handler(ip, port, msg, verbosity=False): @@ -82,7 +105,7 @@ def test_post_handler(ip, port, msg, verbosity=False): # Close HTTP connection sess.close() - return (resp_data == msg) + return test_val("Response mismatch", msg, resp_data) def test_put_handler(ip, port, verbosity=False): @@ -91,31 +114,125 @@ def test_put_handler(ip, port, verbosity=False): verbose_print(verbosity, "Connecting to => " + ip + ":" + port) sess = http.client.HTTPConnection(ip + ":" + port, timeout=15) - # PUT message to /ctrl to disable /hello URI handler - verbose_print(verbosity, "Disabling /hello handler") + # PUT message to /ctrl to disable /hello and /echo URI handlers + # and set 404 error handler to custom http_404_error_handler() + verbose_print(verbosity, "Disabling /hello and /echo handlers") sess.request("PUT", url="/ctrl", body="0") resp = sess.getresponse() resp.read() - sess.request("GET", url="/hello") - resp = sess.getresponse() - resp_data1 = resp.read().decode() - verbose_print(verbosity, "Response on GET /hello : " + resp_data1) + try: + # Send HTTP request to /hello URI + sess.request("GET", url="/hello") + resp = sess.getresponse() + resp_data = resp.read().decode() - # PUT message to /ctrl to enable /hello URI handler - verbose_print(verbosity, "Enabling /hello handler") - sess.request("PUT", url="/ctrl", body="1") - resp = sess.getresponse() - resp.read() + # 404 Error must be returned from server as URI /hello is no longer available. + # But the custom error handler http_404_error_handler() will not close the + # session if the requested URI is /hello + if not test_val("Status code mismatch", 404, resp.status): + raise AssertionError - sess.request("GET", url="/hello") - resp = sess.getresponse() - resp_data2 = resp.read().decode() - verbose_print(verbosity, "Response on GET /hello : " + resp_data2) + # Compare error response string with expectation + verbose_print(verbosity, "Response on GET /hello : " + resp_data) + if not test_val("Response mismatch", "/hello URI is not available", resp_data): + raise AssertionError - # Close HTTP connection - sess.close() - return ((resp_data2 == "Hello World!") and (resp_data1 == "This URI doesn't exist")) + # Using same session for sending an HTTP request to /echo, as it is expected + # that the custom error handler http_404_error_handler() would not have closed + # the session + sess.request("POST", url="/echo", body="Some content") + resp = sess.getresponse() + resp_data = resp.read().decode() + + # 404 Error must be returned from server as URI /hello is no longer available. + # The custom error handler http_404_error_handler() will close the session + # this time as the requested URI is /echo + if not test_val("Status code mismatch", 404, resp.status): + raise AssertionError + + # Compare error response string with expectation + verbose_print(verbosity, "Response on POST /echo : " + resp_data) + if not test_val("Response mismatch", "/echo URI is not available", resp_data): + raise AssertionError + + try: + # Using same session should fail as by now the session would have closed + sess.request("POST", url="/hello", body="Some content") + resp = sess.getresponse() + resp.read().decode() + + # If control reaches this point then the socket was not closed. + # This is not expected + verbose_print(verbosity, "Socket not closed by server") + raise AssertionError + + except http.client.HTTPException: + # Catch socket error as we tried to communicate with an already closed socket + pass + + except http.client.HTTPException: + verbose_print(verbosity, "Socket closed by server") + return False + + except AssertionError: + return False + + finally: + # Close HTTP connection + sess.close() + + verbose_print(verbosity, "Enabling /hello handler") + # Create new connection + sess = http.client.HTTPConnection(ip + ":" + port, timeout=15) + # PUT message to /ctrl to enable /hello URI handler + # and restore 404 error handler to default + sess.request("PUT", url="/ctrl", body="1") + resp = sess.getresponse() + resp.read() + # Close HTTP connection + sess.close() + + # Create new connection + sess = http.client.HTTPConnection(ip + ":" + port, timeout=15) + + try: + # Sending HTTP request to /hello should work now + sess.request("GET", url="/hello") + resp = sess.getresponse() + resp_data = resp.read().decode() + + if not test_val("Status code mismatch", 200, resp.status): + raise AssertionError + + verbose_print(verbosity, "Response on GET /hello : " + resp_data) + if not test_val("Response mismatch", "Hello World!", resp_data): + raise AssertionError + + # 404 Error handler should have been restored to default + sess.request("GET", url="/invalid") + resp = sess.getresponse() + resp_data = resp.read().decode() + + if not test_val("Status code mismatch", 404, resp.status): + raise AssertionError + + verbose_print(verbosity, "Response on GET /invalid : " + resp_data) + if not test_val("Response mismatch", "This URI does not exist", resp_data): + raise AssertionError + + except http.client.HTTPException: + verbose_print(verbosity, "Socket closed by server") + return False + + except AssertionError: + return False + + finally: + # Close HTTP connection + sess.close() + + return True def test_custom_uri_query(ip, port, query, verbosity=False): @@ -138,7 +255,7 @@ def test_custom_uri_query(ip, port, query, verbosity=False): # Close HTTP connection sess.close() - return (resp_data == "Hello World!") + return "Hello World!" == resp_data if __name__ == '__main__': @@ -154,9 +271,9 @@ if __name__ == '__main__': port = args['port'] msg = args['msg'] - if not test_get_handler(ip, port, True): - Utility.console_log("Failed!") - if not test_post_handler(ip, port, msg, True): - Utility.console_log("Failed!") - if not test_put_handler(ip, port, True): + if not ( + test_get_handler(ip, port, True) and + test_put_handler(ip, port, True) and + test_post_handler(ip, port, msg, True) + ): Utility.console_log("Failed!")