From: Anurag Kar Date: Sun, 6 Jan 2019 14:20:20 +0000 (+0530) Subject: HTTP Server : Add uri_match_fn field in config structure which accepts custom URI... X-Git-Tag: v3.3-beta2~105^2~2 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=416c55e7f065e9456320b6a39456761534332cd3;p=esp-idf HTTP Server : Add uri_match_fn field in config structure which accepts custom URI matching functions of type httpd_uri_match_func_t and defaults to basic string compare when set to NULL. Move static uri_matches() function to httpd_uri_match_wildcard() under esp_http_server.h and make it optional. --- diff --git a/components/esp_http_server/include/esp_http_server.h b/components/esp_http_server/include/esp_http_server.h index b3da269f01..7e26753a4c 100644 --- a/components/esp_http_server/include/esp_http_server.h +++ b/components/esp_http_server/include/esp_http_server.h @@ -49,6 +49,7 @@ initializer that should be kept in sync .global_transport_ctx_free_fn = NULL, \ .open_fn = NULL, \ .close_fn = NULL, \ + .uri_match_fn = NULL \ } #define ESP_ERR_HTTPD_BASE (0x8000) /*!< Starting number of HTTPD error codes */ @@ -61,6 +62,10 @@ initializer that should be kept in sync #define ESP_ERR_HTTPD_ALLOC_MEM (ESP_ERR_HTTPD_BASE + 7) /*!< Failed to dynamically allocate memory for resource */ #define ESP_ERR_HTTPD_TASK (ESP_ERR_HTTPD_BASE + 8) /*!< Failed to launch server task/thread */ +/* Symbol to be used as length parameter in httpd_resp_send APIs + * for setting buffer length to string length */ +#define HTTPD_RESP_USE_STRLEN -1 + /* ************** Group: Initialization ************** */ /** @name Initialization * APIs related to the Initialization of the web server @@ -82,7 +87,7 @@ typedef enum http_method httpd_method_t; /** * @brief Prototype for freeing context data (if any) - * @param[in] ctx : object to free + * @param[in] ctx object to free */ typedef void (*httpd_free_ctx_fn_t)(void *ctx); @@ -92,8 +97,8 @@ typedef void (*httpd_free_ctx_fn_t)(void *ctx); * Called immediately after the socket was opened to set up the send/recv functions and * other parameters of the socket. * - * @param[in] hd : server instance - * @param[in] sockfd : session socket file descriptor + * @param[in] hd server instance + * @param[in] sockfd session socket file descriptor * @return status */ typedef esp_err_t (*httpd_open_func_t)(httpd_handle_t hd, int sockfd); @@ -104,11 +109,26 @@ typedef esp_err_t (*httpd_open_func_t)(httpd_handle_t hd, int sockfd); * @note It's possible that the socket descriptor is invalid at this point, the function * is called for all terminated sessions. Ensure proper handling of return codes. * - * @param[in] hd : server instance - * @param[in] sockfd : session socket file descriptor + * @param[in] hd server instance + * @param[in] sockfd session socket file descriptor */ typedef void (*httpd_close_func_t)(httpd_handle_t hd, int sockfd); +/** + * @brief Function prototype for URI matching. + * + * @param[in] reference_uri URI/template with respect to which the other URI is matched + * @param[in] uri_to_match URI/template being matched to the reference URI/template + * @param[in] match_upto For specifying the actual length of `uri_to_match` up to + * which the matching algorithm is to be applied (The maximum + * value is `strlen(uri_to_match)`, independent of the length + * of `reference_uri`) + * @return true on match + */ +typedef bool (*httpd_uri_match_func_t)(const char *reference_uri, + const char *uri_to_match, + size_t match_upto); + /** * @brief HTTP Server Configuration Structure * @@ -195,6 +215,24 @@ typedef struct httpd_config { * was closed by the network stack - that is, the file descriptor may not be valid anymore. */ httpd_close_func_t close_fn; + + /** + * URI matcher function. + * + * Called when searching for a matching URI: + * 1) whose request handler is to be executed right + * after an HTTP request is successfully parsed + * 2) in order to prevent duplication while registering + * a new URI handler using `httpd_register_uri_handler()` + * + * Available options are: + * 1) NULL : Internally do basic matching using `strncmp()` + * 2) `httpd_uri_match_wildcard()` : URI wildcard matcher + * + * Users can implement their own matching functions (See description + * of the `httpd_uri_match_func_t` function prototype) + */ + httpd_uri_match_func_t uri_match_fn; } httpd_config_t; /** @@ -227,8 +265,8 @@ typedef struct httpd_config { * * @endcode * - * @param[in] config : Configuration for new instance of the server - * @param[out] handle : Handle to newly created instance of the server. NULL on error + * @param[in] config Configuration for new instance of the server + * @param[out] handle Handle to newly created instance of the server. NULL on error * @return * - ESP_OK : Instance created successfully * - ESP_ERR_INVALID_ARG : Null argument(s) @@ -451,11 +489,11 @@ esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char* uri); * HTTPD_SOCK_ERR_ codes, which will eventually be conveyed as * return value of httpd_send() function * - * @param[in] hd : server instance - * @param[in] sockfd : session socket file descriptor - * @param[in] buf : buffer with bytes to send - * @param[in] buf_len : data size - * @param[in] flags : flags for the send() function + * @param[in] hd server instance + * @param[in] sockfd session socket file descriptor + * @param[in] buf buffer with bytes to send + * @param[in] buf_len data size + * @param[in] flags flags for the send() function * @return * - Bytes : The number of bytes sent successfully * - HTTPD_SOCK_ERR_INVALID : Invalid arguments @@ -472,11 +510,11 @@ typedef int (*httpd_send_func_t)(httpd_handle_t hd, int sockfd, const char *buf, * HTTPD_SOCK_ERR_ codes, which will eventually be conveyed as * return value of httpd_req_recv() function * - * @param[in] hd : server instance - * @param[in] sockfd : session socket file descriptor - * @param[in] buf : buffer with bytes to send - * @param[in] buf_len : data size - * @param[in] flags : flags for the send() function + * @param[in] hd server instance + * @param[in] sockfd session socket file descriptor + * @param[in] buf buffer with bytes to send + * @param[in] buf_len data size + * @param[in] flags flags for the send() function * @return * - Bytes : The number of bytes received successfully * - 0 : Buffer length parameter is zero / connection closed by peer @@ -494,8 +532,8 @@ typedef int (*httpd_recv_func_t)(httpd_handle_t hd, int sockfd, char *buf, size_ * HTTPD_SOCK_ERR_ codes, which will be handled accordingly in * the server task. * - * @param[in] hd : server instance - * @param[in] sockfd : session socket file descriptor + * @param[in] hd server instance + * @param[in] sockfd session socket file descriptor * @return * - Bytes : The number of bytes waiting to be received * - HTTPD_SOCK_ERR_INVALID : Invalid arguments @@ -744,9 +782,29 @@ esp_err_t httpd_req_get_url_query_str(httpd_req_t *r, char *buf, size_t buf_len) */ esp_err_t httpd_query_key_value(const char *qry, const char *key, char *val, size_t val_size); -/* Symbol to be used as length parameter in httpd_resp_send APIs - * for setting buffer length to string length */ -#define HTTPD_RESP_USE_STRLEN -1 +/** + * @brief Test if a URI matches the given wildcard template. + * + * Template may end with "?" to make the previous character optional (typically a slash), + * "*" for a wildcard match, and "?*" to make the previous character optional, and if present, + * allow anything to follow. + * + * Example: + * - * matches everything + * - /foo/? matches /foo and /foo/ + * - /foo/\* (sans the backslash) matches /foo/ and /foo/bar, but not /foo or /fo + * - /foo/?* or /foo/\*? (sans the backslash) matches /foo/, /foo/bar, and also /foo, but not /foox or /fo + * + * The special characters "?" and "*" anywhere else in the template will be taken literally. + * + * @param[in] template URI template (pattern) + * @param[in] uri URI to be matched + * @param[in] len how many characters of the URI buffer to test + * (there may be trailing query string etc.) + * + * @return true if a match was found + */ +bool httpd_uri_match_wildcard(const char *template, const char *uri, size_t len); /** * @brief API to send a complete HTTP response. diff --git a/components/esp_http_server/src/httpd_uri.c b/components/esp_http_server/src/httpd_uri.c index 9a2df51f9c..4a9841f02a 100644 --- a/components/esp_http_server/src/httpd_uri.c +++ b/components/esp_http_server/src/httpd_uri.c @@ -23,30 +23,13 @@ static const char *TAG = "httpd_uri"; +static bool httpd_uri_match_simple(const char *uri1, const char *uri2, size_t len2) +{ + return strlen(uri1) == len2 && // First match lengths + (strncmp(uri1, uri2, len2) == 0); // Then match actual URIs +} -/** - * @brief Test if a URI matches the given template. - * - * Template may end with "?" to make the previous character optional (typically a slash), - * "*" for a wildcard match, and "?*" to make the previous character optional, and if present, - * allow anything to follow. - * - * Example: - * - * matches everything - * - /foo/? matches /foo and /foo/ - * - /foo/\* (sans the backslash) matches /foo/ and /foo/bar, but not /foo or /fo - * - /foo/?* or /foo/\*? (sans the backslash) matches /foo/, /foo/bar, and also /foo, but not /foox or /fo - * - * The special characters "?" and "*" anywhere else in the template will be taken literally. - * - * @param[in] template - URI template (pattern) - * @param[in] uri - tested URI - * @param[in] template - how many characters of the URI buffer to test - * (there may be trailing query string etc.) - * - * @return true if a match was found - */ -static bool uri_matches(const char *template, const char *uri, const unsigned int len) +bool httpd_uri_match_wildcard(const char *template, const char *uri, size_t len) { const size_t tpl_len = strlen(template); size_t exact_match_chars = tpl_len; @@ -57,12 +40,27 @@ static bool uri_matches(const char *template, const char *uri, const unsigned in const bool asterisk = last == '*' || (prevlast == '*' && last == '?'); const bool quest = last == '?' || (prevlast == '?' && last == '*'); + /* Minimum template string length must be: + * 0 : if neither of '*' and '?' are present + * 1 : if only '*' is present + * 2 : if only '?' is present + * 3 : if both are present + * + * The expression (asterisk + quest*2) serves as a + * case wise generator of these length values + */ + /* abort in cases such as "?" with no preceding character (invalid template) */ - if (exact_match_chars < asterisk + quest*2) return false; + if (exact_match_chars < asterisk + quest*2) { + return false; + } + /* account for special characters and the optional character if "?" is used */ exact_match_chars -= asterisk + quest*2; - if (len < exact_match_chars) return false; + if (len < exact_match_chars) { + return false; + } if (!quest) { if (!asterisk && len != exact_match_chars) { @@ -71,14 +69,14 @@ static bool uri_matches(const char *template, const char *uri, const unsigned in } /* asterisk allows arbitrary trailing characters, we ignore these using * exact_match_chars as the length limit */ - return 0 == strncmp(template, uri, exact_match_chars); + return (strncmp(template, uri, exact_match_chars) == 0); } else { /* question mark present */ if (len > exact_match_chars && template[exact_match_chars] != uri[exact_match_chars]) { /* the optional character is present, but different */ return false; } - if (0 != strncmp(template, uri, exact_match_chars)) { + if (strncmp(template, uri, exact_match_chars) != 0) { /* the mandatory part differs */ return false; } @@ -90,20 +88,47 @@ static bool uri_matches(const char *template, const char *uri, const unsigned in } } -static int httpd_find_uri_handler(struct httpd_data *hd, - const char* uri, - httpd_method_t method) +/* Find handler with matching URI and method, and set + * appropriate error code if URI or method not found */ +static httpd_uri_t* httpd_find_uri_handler(struct httpd_data *hd, + const char *uri, size_t uri_len, + httpd_method_t method, + httpd_err_resp_t *err) { + if (err) { + *err = HTTPD_404_NOT_FOUND; + } + for (int i = 0; i < hd->config.max_uri_handlers; i++) { - if (hd->hd_calls[i]) { - ESP_LOGD(TAG, LOG_FMT("[%d] = %s"), i, hd->hd_calls[i]->uri); - if ((hd->hd_calls[i]->method == method) && // First match methods - uri_matches(hd->hd_calls[i]->uri, uri, strlen(uri))) { // Then match uri strings - return i; + if (!hd->hd_calls[i]) { + break; + } + ESP_LOGD(TAG, LOG_FMT("[%d] = %s"), i, hd->hd_calls[i]->uri); + + /* Check if custom URI matching function is set, + * else use simple string compare */ + if (hd->config.uri_match_fn ? + hd->config.uri_match_fn(hd->hd_calls[i]->uri, uri, uri_len) : + httpd_uri_match_simple(hd->hd_calls[i]->uri, uri, uri_len)) { + /* URIs match. Now check if method is supported */ + if (hd->hd_calls[i]->method == method) { + /* Match found! */ + if (err) { + /* Unset any error that may + * have been set earlier */ + *err = 0; + } + return hd->hd_calls[i]; + } + /* URI found but method not allowed. + * If URI is found later then this + * error must be set to 0 */ + if (err) { + *err = HTTPD_405_METHOD_NOT_ALLOWED; } } } - return -1; + return NULL; } esp_err_t httpd_register_uri_handler(httpd_handle_t handle, @@ -115,11 +140,13 @@ esp_err_t httpd_register_uri_handler(httpd_handle_t handle, struct httpd_data *hd = (struct httpd_data *) handle; - /* Make sure another handler with same URI and method - * is not already registered - */ + /* Make sure another handler with matching URI and method + * is not already registered. This will also catch cases + * when a registered URI wildcard pattern already accounts + * for the new URI being registered */ if (httpd_find_uri_handler(handle, uri_handler->uri, - uri_handler->method) != -1) { + strlen(uri_handler->uri), + uri_handler->method, NULL) != NULL) { ESP_LOGW(TAG, LOG_FMT("handler %s with method %d already registered"), uri_handler->uri, uri_handler->method); return ESP_ERR_HTTPD_HANDLER_EXISTS; @@ -162,15 +189,30 @@ esp_err_t httpd_unregister_uri_handler(httpd_handle_t handle, } struct httpd_data *hd = (struct httpd_data *) handle; - int i = httpd_find_uri_handler(hd, uri, method); + for (int i = 0; i < hd->config.max_uri_handlers; i++) { + if (!hd->hd_calls[i]) { + break; + } + if ((hd->hd_calls[i]->method == method) && // First match methods + (strcmp(hd->hd_calls[i]->uri, uri) == 0)) { // Then match URI string + ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, hd->hd_calls[i]->uri); - if (i != -1) { - ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, hd->hd_calls[i]->uri); + free((char*)hd->hd_calls[i]->uri); + free(hd->hd_calls[i]); + hd->hd_calls[i] = NULL; - free((char*)hd->hd_calls[i]->uri); - free(hd->hd_calls[i]); - hd->hd_calls[i] = NULL; - return ESP_OK; + /* Shift the remaining non null handlers in the array + * forward by 1 so that order of insertion is maintained */ + for (i += 1; i < hd->config.max_uri_handlers; i++) { + if (!hd->hd_calls[i]) { + break; + } + hd->hd_calls[i-1] = hd->hd_calls[i]; + } + /* Nullify the following non null entry */ + hd->hd_calls[i-1] = NULL; + return ESP_OK; + } } ESP_LOGW(TAG, LOG_FMT("handler %s with method %d not found"), uri, method); return ESP_ERR_NOT_FOUND; @@ -185,17 +227,31 @@ esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char *uri) struct httpd_data *hd = (struct httpd_data *) handle; bool found = false; - for (int i = 0; i < hd->config.max_uri_handlers; i++) { - if ((hd->hd_calls[i] != NULL) && - (strcmp(hd->hd_calls[i]->uri, uri) == 0)) { + int i = 0, j = 0; // For keeping count of removed entries + for (; i < hd->config.max_uri_handlers; i++) { + if (!hd->hd_calls[i]) { + break; + } + if (strcmp(hd->hd_calls[i]->uri, uri) == 0) { // Match URI strings ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, uri); free((char*)hd->hd_calls[i]->uri); free(hd->hd_calls[i]); hd->hd_calls[i] = NULL; found = true; + + j++; // Update count of removed entries + } else { + /* Shift the remaining non null handlers in the array + * forward by j so that order of insertion is maintained */ + hd->hd_calls[i-j] = hd->hd_calls[i]; } } + /* Nullify the following non null entries */ + for (int k = (i - j); k < i; k++) { + hd->hd_calls[k] = NULL; + } + if (!found) { ESP_LOGW(TAG, LOG_FMT("no handler found for URI %s"), uri); } @@ -205,44 +261,15 @@ esp_err_t httpd_unregister_uri(httpd_handle_t handle, const char *uri) void httpd_unregister_all_uri_handlers(struct httpd_data *hd) { for (unsigned i = 0; i < hd->config.max_uri_handlers; i++) { - if (hd->hd_calls[i]) { - ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, hd->hd_calls[i]->uri); - - free((char*)hd->hd_calls[i]->uri); - free(hd->hd_calls[i]); + if (!hd->hd_calls[i]) { + break; } - } -} + ESP_LOGD(TAG, LOG_FMT("[%d] removing %s"), i, hd->hd_calls[i]->uri); -/* Alternate implmentation of httpd_find_uri_handler() - * which takes a uri_len field. This is useful when the URI - * string contains extra parameters that are not to be included - * while matching with the registered URI_handler strings - */ -static httpd_uri_t* httpd_find_uri_handler2(httpd_err_resp_t *err, - struct httpd_data *hd, - const char *uri, size_t uri_len, - httpd_method_t method) -{ - *err = 0; - for (int i = 0; i < hd->config.max_uri_handlers; i++) { - if (hd->hd_calls[i]) { - ESP_LOGD(TAG, LOG_FMT("[%d] = %s"), i, hd->hd_calls[i]->uri); - if (uri_matches(hd->hd_calls[i]->uri, uri, uri_len)) { - if (hd->hd_calls[i]->method == method) { // Match methods - return hd->hd_calls[i]; - } - /* URI found but method not allowed. - * If URI IS found later then this - * error is to be neglected */ - *err = HTTPD_405_METHOD_NOT_ALLOWED; - } - } - } - if (*err == 0) { - *err = HTTPD_404_NOT_FOUND; + free((char*)hd->hd_calls[i]->uri); + free(hd->hd_calls[i]); + hd->hd_calls[i] = NULL; } - return NULL; } esp_err_t httpd_uri(struct httpd_data *hd) @@ -255,12 +282,11 @@ esp_err_t httpd_uri(struct httpd_data *hd) httpd_err_resp_t err = 0; ESP_LOGD(TAG, LOG_FMT("request for %s with type %d"), req->uri, req->method); + /* URL parser result contains offset and length of path string */ if (res->field_set & (1 << UF_PATH)) { - uri = httpd_find_uri_handler2(&err, hd, - req->uri + res->field_data[UF_PATH].off, - res->field_data[UF_PATH].len, - req->method); + uri = httpd_find_uri_handler(hd, req->uri + res->field_data[UF_PATH].off, + res->field_data[UF_PATH].len, req->method, &err); } /* If URI with method not found, respond with error code */ @@ -270,7 +296,8 @@ esp_err_t httpd_uri(struct httpd_data *hd) ESP_LOGW(TAG, LOG_FMT("URI '%s' not found"), req->uri); return httpd_resp_send_err(req, HTTPD_404_NOT_FOUND); case HTTPD_405_METHOD_NOT_ALLOWED: - ESP_LOGW(TAG, LOG_FMT("Method '%d' not allowed for URI '%s'"), req->method, req->uri); + ESP_LOGW(TAG, LOG_FMT("Method '%d' not allowed for URI '%s'"), + req->method, req->uri); return httpd_resp_send_err(req, HTTPD_405_METHOD_NOT_ALLOWED); default: return ESP_FAIL;