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.
This commit is contained in:
Anurag Kar 2019-01-06 19:50:20 +05:30
parent 107f52c4fc
commit 416c55e7f0
2 changed files with 197 additions and 112 deletions

View file

@ -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.

View file

@ -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;
}
}
free((char*)hd->hd_calls[i]->uri);
free(hd->hd_calls[i]);
hd->hd_calls[i] = NULL;
}
if (*err == 0) {
*err = HTTPD_404_NOT_FOUND;
}
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;