This MR is intended to fix incorrect parsing of HTTP requests when empty header values are present. The issue is was due to asymmetric behavior of `http_parser` library, which in case of: non-empty header values : invokes callbacks with the pointer to the start of a value empty header values : invokes callbacks with pointer to the start of next header or section Since HTTP server relies on this pointer (along with length of the value) to locate the end of a value, and replace the line terminators (CRLFs) with null characters, the second case needed to be handled correctly. Closes IDFGH-1539 Closes https://github.com/espressif/esp-idf/issues/3803
1002 lines
34 KiB
C
1002 lines
34 KiB
C
// Copyright 2018 Espressif Systems (Shanghai) PTE LTD
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
|
|
#include <stdlib.h>
|
|
#include <sys/param.h>
|
|
#include <esp_log.h>
|
|
#include <esp_err.h>
|
|
#include <http_parser.h>
|
|
|
|
#include <esp_http_server.h>
|
|
#include "esp_httpd_priv.h"
|
|
#include "osal.h"
|
|
|
|
static const char *TAG = "httpd_parse";
|
|
|
|
typedef struct {
|
|
/* Parser settings for http_parser_execute() */
|
|
http_parser_settings settings;
|
|
|
|
/* Request being parsed */
|
|
struct httpd_req *req;
|
|
|
|
/* Status of the parser describes the part of the
|
|
* HTTP request packet being processed at any moment.
|
|
*/
|
|
enum {
|
|
PARSING_IDLE = 0,
|
|
PARSING_URL,
|
|
PARSING_HDR_FIELD,
|
|
PARSING_HDR_VALUE,
|
|
PARSING_BODY,
|
|
PARSING_COMPLETE,
|
|
PARSING_FAILED
|
|
} status;
|
|
|
|
/* Response error code in case of PARSING_FAILED */
|
|
httpd_err_code_t error;
|
|
|
|
/* For storing last callback parameters */
|
|
struct {
|
|
const char *at;
|
|
size_t length;
|
|
} last;
|
|
|
|
/* State variables */
|
|
bool paused; /*!< Parser is paused */
|
|
size_t pre_parsed; /*!< Length of data to be skipped while parsing */
|
|
size_t raw_datalen; /*!< Full length of the raw data in scratch buffer */
|
|
} parser_data_t;
|
|
|
|
static esp_err_t verify_url (http_parser *parser)
|
|
{
|
|
parser_data_t *parser_data = (parser_data_t *) parser->data;
|
|
struct httpd_req *r = parser_data->req;
|
|
struct httpd_req_aux *ra = r->aux;
|
|
struct http_parser_url *res = &ra->url_parse_res;
|
|
|
|
/* Get previous values of the parser callback arguments */
|
|
const char *at = parser_data->last.at;
|
|
size_t length = parser_data->last.length;
|
|
|
|
if ((r->method = parser->method) < 0) {
|
|
ESP_LOGW(TAG, LOG_FMT("HTTP Operation not supported"));
|
|
parser_data->error = HTTPD_501_METHOD_NOT_IMPLEMENTED;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
if (sizeof(r->uri) < (length + 1)) {
|
|
ESP_LOGW(TAG, LOG_FMT("URI length (%d) greater than supported (%d)"),
|
|
length, sizeof(r->uri));
|
|
parser_data->error = HTTPD_414_URI_TOO_LONG;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
/* Keep URI with terminating null character. Note URI string pointed
|
|
* by 'at' is not NULL terminated, therefore use length provided by
|
|
* parser while copying the URI to buffer */
|
|
strlcpy((char *)r->uri, at, (length + 1));
|
|
ESP_LOGD(TAG, LOG_FMT("received URI = %s"), r->uri);
|
|
|
|
/* Make sure version is HTTP/1.1 */
|
|
if ((parser->http_major != 1) && (parser->http_minor != 1)) {
|
|
ESP_LOGW(TAG, LOG_FMT("unsupported HTTP version = %d.%d"),
|
|
parser->http_major, parser->http_minor);
|
|
parser_data->error = HTTPD_505_VERSION_NOT_SUPPORTED;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
/* Parse URL and keep result for later */
|
|
http_parser_url_init(res);
|
|
if (http_parser_parse_url(r->uri, strlen(r->uri),
|
|
r->method == HTTP_CONNECT, res)) {
|
|
ESP_LOGW(TAG, LOG_FMT("http_parser_parse_url failed with errno = %d"),
|
|
parser->http_errno);
|
|
parser_data->error = HTTPD_400_BAD_REQUEST;
|
|
return ESP_FAIL;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* http_parser callback on finding url in HTTP request
|
|
* Will be invoked ATLEAST once every packet
|
|
*/
|
|
static esp_err_t cb_url(http_parser *parser,
|
|
const char *at, size_t length)
|
|
{
|
|
parser_data_t *parser_data = (parser_data_t *) parser->data;
|
|
|
|
if (parser_data->status == PARSING_IDLE) {
|
|
ESP_LOGD(TAG, LOG_FMT("message begin"));
|
|
|
|
/* Store current values of the parser callback arguments */
|
|
parser_data->last.at = at;
|
|
parser_data->last.length = 0;
|
|
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;
|
|
}
|
|
|
|
ESP_LOGD(TAG, LOG_FMT("processing url = %.*s"), length, at);
|
|
|
|
/* Update length of URL string */
|
|
if ((parser_data->last.length += length) > HTTPD_MAX_URI_LEN) {
|
|
ESP_LOGW(TAG, LOG_FMT("URI length (%d) greater than supported (%d)"),
|
|
parser_data->last.length, HTTPD_MAX_URI_LEN);
|
|
parser_data->error = HTTPD_414_URI_TOO_LONG;
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
static esp_err_t pause_parsing(http_parser *parser, const char* at)
|
|
{
|
|
parser_data_t *parser_data = (parser_data_t *) parser->data;
|
|
struct httpd_req *r = parser_data->req;
|
|
struct httpd_req_aux *ra = r->aux;
|
|
|
|
/* The length of data that was not parsed due to interruption
|
|
* and hence needs to be read again later for parsing */
|
|
ssize_t unparsed = parser_data->raw_datalen - (at - ra->scratch);
|
|
if (unparsed < 0) {
|
|
ESP_LOGE(TAG, LOG_FMT("parsing beyond valid data = %d"), -unparsed);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
/* Push back the un-parsed data into pending buffer for
|
|
* receiving again with httpd_recv_with_opt() later when
|
|
* read_block() executes */
|
|
if (unparsed && (unparsed != httpd_unrecv(r, at, unparsed))) {
|
|
ESP_LOGE(TAG, LOG_FMT("data too large for un-recv = %d"), unparsed);
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
/* Signal http_parser to pause execution and save the maximum
|
|
* possible length, of the yet un-parsed data, that may get
|
|
* parsed before http_parser_execute() returns. This pre_parsed
|
|
* length will be updated then to reflect the actual length
|
|
* that got parsed, and must be skipped when parsing resumes */
|
|
parser_data->pre_parsed = unparsed;
|
|
http_parser_pause(parser, 1);
|
|
parser_data->paused = true;
|
|
ESP_LOGD(TAG, LOG_FMT("paused"));
|
|
return ESP_OK;
|
|
}
|
|
|
|
static size_t continue_parsing(http_parser *parser, size_t length)
|
|
{
|
|
parser_data_t *data = (parser_data_t *) parser->data;
|
|
|
|
/* Part of the received data may have been parsed earlier
|
|
* so we must skip that before parsing resumes */
|
|
length = MIN(length, data->pre_parsed);
|
|
data->pre_parsed -= length;
|
|
ESP_LOGD(TAG, LOG_FMT("skip pre-parsed data of size = %d"), length);
|
|
|
|
http_parser_pause(parser, 0);
|
|
data->paused = false;
|
|
ESP_LOGD(TAG, LOG_FMT("un-paused"));
|
|
return length;
|
|
}
|
|
|
|
/* http_parser callback on header field in HTTP request
|
|
* May be invoked ATLEAST once every header field
|
|
*/
|
|
static esp_err_t cb_header_field(http_parser *parser, const char *at, size_t length)
|
|
{
|
|
parser_data_t *parser_data = (parser_data_t *) parser->data;
|
|
struct httpd_req *r = parser_data->req;
|
|
struct httpd_req_aux *ra = r->aux;
|
|
|
|
/* Check previous status */
|
|
if (parser_data->status == PARSING_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;
|
|
}
|
|
|
|
ESP_LOGD(TAG, LOG_FMT("headers begin"));
|
|
/* Last at is set to start of scratch where headers
|
|
* will be received next */
|
|
parser_data->last.at = ra->scratch;
|
|
parser_data->last.length = 0;
|
|
parser_data->status = PARSING_HDR_FIELD;
|
|
|
|
/* Stop parsing for now and give control to process */
|
|
if (pause_parsing(parser, at) != ESP_OK) {
|
|
parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR;
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
} else if (parser_data->status == PARSING_HDR_VALUE) {
|
|
/* Overwrite terminator (CRLFs) following last header
|
|
* (key: value) pair with null characters */
|
|
char *term_start = (char *)parser_data->last.at + parser_data->last.length;
|
|
memset(term_start, '\0', at - term_start);
|
|
|
|
/* Store current values of the parser callback arguments */
|
|
parser_data->last.at = at;
|
|
parser_data->last.length = 0;
|
|
parser_data->status = PARSING_HDR_FIELD;
|
|
|
|
/* Increment header count */
|
|
ra->req_hdrs_count++;
|
|
} else if (parser_data->status != PARSING_HDR_FIELD) {
|
|
ESP_LOGE(TAG, LOG_FMT("unexpected state transition"));
|
|
parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR;
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
ESP_LOGD(TAG, LOG_FMT("processing field = %.*s"), length, at);
|
|
|
|
/* Update length of header string */
|
|
parser_data->last.length += length;
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* http_parser callback on header value in HTTP request.
|
|
* May be invoked ATLEAST once every header value
|
|
*/
|
|
static esp_err_t cb_header_value(http_parser *parser, const char *at, size_t length)
|
|
{
|
|
parser_data_t *parser_data = (parser_data_t *) parser->data;
|
|
|
|
/* Check previous status */
|
|
if (parser_data->status == PARSING_HDR_FIELD) {
|
|
/* Store current values of the parser callback arguments */
|
|
parser_data->last.at = at;
|
|
parser_data->last.length = 0;
|
|
parser_data->status = PARSING_HDR_VALUE;
|
|
|
|
if (length == 0) {
|
|
/* As per behavior of http_parser, when length > 0,
|
|
* `at` points to the start of CRLF. But, in the
|
|
* case when header value is empty (zero length),
|
|
* then `at` points to the position right after
|
|
* the CRLF. Since for our purpose we need `last.at`
|
|
* to point to exactly where the CRLF starts, it
|
|
* needs to be adjusted by the right offset */
|
|
char *at_adj = (char *)parser_data->last.at;
|
|
/* Find the end of header field string */
|
|
while (*(--at_adj) != ':');
|
|
/* Now skip leading spaces' */
|
|
while (*(++at_adj) == ' ');
|
|
/* Now we are at the right position */
|
|
parser_data->last.at = at_adj;
|
|
}
|
|
} else if (parser_data->status != PARSING_HDR_VALUE) {
|
|
ESP_LOGE(TAG, LOG_FMT("unexpected state transition"));
|
|
parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR;
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
ESP_LOGD(TAG, LOG_FMT("processing value = %.*s"), length, at);
|
|
|
|
/* Update length of header string */
|
|
parser_data->last.length += length;
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* http_parser callback on completing headers in HTTP request.
|
|
* Will be invoked ONLY once every packet
|
|
*/
|
|
static esp_err_t cb_headers_complete(http_parser *parser)
|
|
{
|
|
parser_data_t *parser_data = (parser_data_t *) parser->data;
|
|
struct httpd_req *r = parser_data->req;
|
|
struct httpd_req_aux *ra = r->aux;
|
|
|
|
/* Check previous status */
|
|
if (parser_data->status == PARSING_URL) {
|
|
ESP_LOGD(TAG, LOG_FMT("no headers"));
|
|
if (verify_url(parser) != ESP_OK) {
|
|
/* verify_url would already have set the
|
|
* error field of parser data, so only setting
|
|
* status to failed */
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
} else if (parser_data->status == PARSING_HDR_VALUE) {
|
|
/* Locate end of last header */
|
|
char *at = (char *)parser_data->last.at + parser_data->last.length;
|
|
|
|
/* Check if there is data left to parse. This value should
|
|
* at least be equal to the number of line terminators, i.e. 2 */
|
|
ssize_t remaining_length = parser_data->raw_datalen - (at - ra->scratch);
|
|
if (remaining_length < 2) {
|
|
ESP_LOGE(TAG, LOG_FMT("invalid length of data remaining to be parsed"));
|
|
parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR;
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
/* Locate end of headers section by skipping the remaining
|
|
* two line terminators. No assumption is made here about the
|
|
* termination sequence used apart from the necessity that it
|
|
* must end with an LF, because:
|
|
* 1) some clients may send non standard LFs instead of
|
|
* CRLFs for indicating termination.
|
|
* 2) it is the responsibility of http_parser to check
|
|
* that the termination is either CRLF or LF and
|
|
* not any other sequence */
|
|
unsigned short remaining_terminators = 2;
|
|
while (remaining_length-- && remaining_terminators) {
|
|
if (*at == '\n') {
|
|
remaining_terminators--;
|
|
}
|
|
/* Overwrite termination characters with null */
|
|
*(at++) = '\0';
|
|
}
|
|
if (remaining_terminators) {
|
|
ESP_LOGE(TAG, LOG_FMT("incomplete termination of headers"));
|
|
parser_data->error = HTTPD_400_BAD_REQUEST;
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
/* Place the parser ptr right after the end of headers section */
|
|
parser_data->last.at = at;
|
|
|
|
/* Increment header count */
|
|
ra->req_hdrs_count++;
|
|
} else {
|
|
ESP_LOGE(TAG, LOG_FMT("unexpected state transition"));
|
|
parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR;
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
/* In absence of body/chunked encoding, http_parser sets content_len to -1 */
|
|
r->content_len = ((int)parser->content_length != -1 ?
|
|
parser->content_length : 0);
|
|
|
|
ESP_LOGD(TAG, LOG_FMT("bytes read = %d"), parser->nread);
|
|
ESP_LOGD(TAG, LOG_FMT("content length = %zu"), r->content_len);
|
|
|
|
if (parser->upgrade) {
|
|
ESP_LOGW(TAG, LOG_FMT("upgrade from HTTP 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;
|
|
}
|
|
|
|
parser_data->status = PARSING_BODY;
|
|
ra->remaining_len = r->content_len;
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* Last http_parser callback if body present in HTTP request.
|
|
* Will be invoked ONLY once every packet
|
|
*/
|
|
static esp_err_t cb_on_body(http_parser *parser, const char *at, size_t length)
|
|
{
|
|
parser_data_t *parser_data = (parser_data_t *) parser->data;
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* Pause parsing so that if part of another packet
|
|
* is in queue then it doesn't get parsed, which
|
|
* may reset the parser state and cause current
|
|
* request packet to be lost */
|
|
if (pause_parsing(parser, at) != ESP_OK) {
|
|
parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR;
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
parser_data->last.at = 0;
|
|
parser_data->last.length = 0;
|
|
parser_data->status = PARSING_COMPLETE;
|
|
ESP_LOGD(TAG, LOG_FMT("body begins"));
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* Last http_parser callback if body absent in HTTP request.
|
|
* Will be invoked ONLY once every packet
|
|
*/
|
|
static esp_err_t cb_no_body(http_parser *parser)
|
|
{
|
|
parser_data_t *parser_data = (parser_data_t *) parser->data;
|
|
|
|
/* Check previous status */
|
|
if (parser_data->status == PARSING_URL) {
|
|
ESP_LOGD(TAG, LOG_FMT("no headers"));
|
|
if (verify_url(parser) != ESP_OK) {
|
|
/* verify_url would already have set the
|
|
* error field of parser data, so only setting
|
|
* status to failed */
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
} else if (parser_data->status != PARSING_BODY) {
|
|
ESP_LOGE(TAG, LOG_FMT("unexpected state transition"));
|
|
parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR;
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
/* Pause parsing so that if part of another packet
|
|
* is in queue then it doesn't get parsed, which
|
|
* may reset the parser state and cause current
|
|
* request packet to be lost */
|
|
if (pause_parsing(parser, parser_data->last.at) != ESP_OK) {
|
|
parser_data->error = HTTPD_500_INTERNAL_SERVER_ERROR;
|
|
parser_data->status = PARSING_FAILED;
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
parser_data->last.at = 0;
|
|
parser_data->last.length = 0;
|
|
parser_data->status = PARSING_COMPLETE;
|
|
ESP_LOGD(TAG, LOG_FMT("message complete"));
|
|
return ESP_OK;
|
|
}
|
|
|
|
static int read_block(httpd_req_t *req, size_t offset, size_t length)
|
|
{
|
|
struct httpd_req_aux *raux = req->aux;
|
|
|
|
/* Limits the read to scratch buffer size */
|
|
ssize_t buf_len = MIN(length, (sizeof(raux->scratch) - offset));
|
|
if (buf_len <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
/* Receive data into buffer. If data is pending (from unrecv) then return
|
|
* immediately after receiving pending data, as pending data may just complete
|
|
* this request packet. */
|
|
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) {
|
|
/* 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;
|
|
}
|
|
/* 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"));
|
|
/* 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);
|
|
return nbytes;
|
|
}
|
|
|
|
static int parse_block(http_parser *parser, size_t offset, size_t length)
|
|
{
|
|
parser_data_t *data = (parser_data_t *)(parser->data);
|
|
httpd_req_t *req = data->req;
|
|
struct httpd_req_aux *raux = req->aux;
|
|
size_t nparsed = 0;
|
|
|
|
if (!length) {
|
|
/* 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;
|
|
break;
|
|
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;
|
|
}
|
|
|
|
/* Un-pause the parsing if paused */
|
|
if (data->paused) {
|
|
nparsed = continue_parsing(parser, length);
|
|
length -= nparsed;
|
|
offset += nparsed;
|
|
if (!length) {
|
|
return nparsed;
|
|
}
|
|
}
|
|
|
|
/* Execute http_parser */
|
|
nparsed = http_parser_execute(parser, &data->settings,
|
|
raux->scratch + offset, 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) {
|
|
/* Update the value of pre_parsed which was set when
|
|
* pause_parsing() was called. (length - nparsed) is
|
|
* the length of the data that will need to be parsed
|
|
* again later and hence must be deducted from the
|
|
* pre_parsed length */
|
|
data->pre_parsed -= (length - nparsed);
|
|
return 0;
|
|
} else if (nparsed != length) {
|
|
/* http_parser error */
|
|
data->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;
|
|
}
|
|
|
|
/* Return with the total length of the request packet
|
|
* that has been parsed till now */
|
|
ESP_LOGD(TAG, LOG_FMT("parsed block size = %d"), offset + nparsed);
|
|
return offset + nparsed;
|
|
}
|
|
|
|
static void parse_init(httpd_req_t *r, http_parser *parser, parser_data_t *data)
|
|
{
|
|
/* Initialize parser data */
|
|
memset(data, 0, sizeof(parser_data_t));
|
|
data->req = r;
|
|
|
|
/* Initialize parser */
|
|
http_parser_init(parser, HTTP_REQUEST);
|
|
parser->data = (void *)data;
|
|
|
|
/* Initialize parser settings */
|
|
http_parser_settings_init(&data->settings);
|
|
|
|
/* Set parser callbacks */
|
|
data->settings.on_url = cb_url;
|
|
data->settings.on_header_field = cb_header_field;
|
|
data->settings.on_header_value = cb_header_value;
|
|
data->settings.on_headers_complete = cb_headers_complete;
|
|
data->settings.on_body = cb_on_body;
|
|
data->settings.on_message_complete = cb_no_body;
|
|
}
|
|
|
|
/* Function that receives TCP data and runs parser on it
|
|
*/
|
|
static esp_err_t httpd_parse_req(struct httpd_data *hd)
|
|
{
|
|
httpd_req_t *r = &hd->hd_req;
|
|
int blk_len, offset;
|
|
http_parser parser;
|
|
parser_data_t parser_data;
|
|
|
|
/* Initialize parser */
|
|
parse_init(r, &parser, &parser_data);
|
|
|
|
/* Set offset to start of scratch buffer */
|
|
offset = 0;
|
|
do {
|
|
/* Read block into scratch buffer */
|
|
if ((blk_len = read_block(r, offset, PARSER_BLOCK_SIZE)) < 0) {
|
|
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;
|
|
}
|
|
|
|
/* This is used by the callbacks to track
|
|
* data usage of the buffer */
|
|
parser_data.raw_datalen = blk_len + offset;
|
|
|
|
/* Parse data block from buffer */
|
|
if ((offset = parse_block(&parser, offset, blk_len)) < 0) {
|
|
/* 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);
|
|
|
|
ESP_LOGD(TAG, LOG_FMT("parsing complete"));
|
|
return httpd_uri(hd);
|
|
}
|
|
|
|
static void init_req(httpd_req_t *r, httpd_config_t *config)
|
|
{
|
|
r->handle = 0;
|
|
r->method = 0;
|
|
memset((char*)r->uri, 0, sizeof(r->uri));
|
|
r->content_len = 0;
|
|
r->aux = 0;
|
|
r->user_ctx = 0;
|
|
r->sess_ctx = 0;
|
|
r->free_ctx = 0;
|
|
r->ignore_sess_ctx_changes = 0;
|
|
}
|
|
|
|
static void init_req_aux(struct httpd_req_aux *ra, httpd_config_t *config)
|
|
{
|
|
ra->sd = 0;
|
|
memset(ra->scratch, 0, sizeof(ra->scratch));
|
|
ra->remaining_len = 0;
|
|
ra->status = 0;
|
|
ra->content_type = 0;
|
|
ra->first_chunk_sent = 0;
|
|
ra->req_hdrs_count = 0;
|
|
ra->resp_hdrs_count = 0;
|
|
memset(ra->resp_hdrs, 0, config->max_resp_headers * sizeof(struct resp_hdr));
|
|
}
|
|
|
|
static void httpd_req_cleanup(httpd_req_t *r)
|
|
{
|
|
struct httpd_req_aux *ra = r->aux;
|
|
|
|
/* Check if the context has changed and needs to be cleared */
|
|
if ((r->ignore_sess_ctx_changes == false) && (ra->sd->ctx != r->sess_ctx)) {
|
|
httpd_sess_free_ctx(ra->sd->ctx, ra->sd->free_ctx);
|
|
}
|
|
/* Retrieve session info from the request into the socket database. */
|
|
ra->sd->ctx = r->sess_ctx;
|
|
ra->sd->free_ctx = r->free_ctx;
|
|
ra->sd->ignore_sess_ctx_changes = r->ignore_sess_ctx_changes;
|
|
|
|
/* Clear out the request and request_aux structures */
|
|
ra->sd = NULL;
|
|
r->handle = NULL;
|
|
r->aux = NULL;
|
|
}
|
|
|
|
/* Function that processes incoming TCP data and
|
|
* updates the http request data httpd_req_t
|
|
*/
|
|
esp_err_t httpd_req_new(struct httpd_data *hd, struct sock_db *sd)
|
|
{
|
|
httpd_req_t *r = &hd->hd_req;
|
|
init_req(r, &hd->config);
|
|
init_req_aux(&hd->hd_req_aux, &hd->config);
|
|
r->handle = hd;
|
|
r->aux = &hd->hd_req_aux;
|
|
/* Associate the request to the socket */
|
|
struct httpd_req_aux *ra = r->aux;
|
|
ra->sd = sd;
|
|
/* Set defaults */
|
|
ra->status = (char *)HTTPD_200;
|
|
ra->content_type = (char *)HTTPD_TYPE_TEXT;
|
|
ra->first_chunk_sent = false;
|
|
/* Copy session info to the request */
|
|
r->sess_ctx = sd->ctx;
|
|
r->free_ctx = sd->free_ctx;
|
|
r->ignore_sess_ctx_changes = sd->ignore_sess_ctx_changes;
|
|
/* Parse request */
|
|
esp_err_t err = httpd_parse_req(hd);
|
|
if (err != ESP_OK) {
|
|
httpd_req_cleanup(r);
|
|
}
|
|
return err;
|
|
}
|
|
|
|
/* Function that resets the http request data
|
|
*/
|
|
esp_err_t httpd_req_delete(struct httpd_data *hd)
|
|
{
|
|
httpd_req_t *r = &hd->hd_req;
|
|
struct httpd_req_aux *ra = r->aux;
|
|
|
|
/* Finish off reading any pending/leftover data */
|
|
while (ra->remaining_len) {
|
|
/* Any length small enough not to overload the stack, but large
|
|
* enough to finish off the buffers fast */
|
|
char dummy[CONFIG_HTTPD_PURGE_BUF_LEN];
|
|
int recv_len = MIN(sizeof(dummy), ra->remaining_len);
|
|
recv_len = httpd_req_recv(r, dummy, recv_len);
|
|
if (recv_len < 0) {
|
|
httpd_req_cleanup(r);
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
ESP_LOGD(TAG, LOG_FMT("purging data size : %d bytes"), recv_len);
|
|
|
|
#ifdef CONFIG_HTTPD_LOG_PURGE_DATA
|
|
/* Enabling this will log discarded binary HTTP content data at
|
|
* Debug level. For large content data this may not be desirable
|
|
* as it will clutter the log */
|
|
ESP_LOGD(TAG, "================= PURGED DATA =================");
|
|
ESP_LOG_BUFFER_HEX_LEVEL(TAG, dummy, recv_len, ESP_LOG_DEBUG);
|
|
ESP_LOGD(TAG, "===============================================");
|
|
#endif
|
|
}
|
|
|
|
httpd_req_cleanup(r);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/* Validates the request to prevent users from calling APIs, that are to
|
|
* be called only inside URI handler, outside the handler context
|
|
*/
|
|
bool httpd_validate_req_ptr(httpd_req_t *r)
|
|
{
|
|
if (r) {
|
|
struct httpd_data *hd = (struct httpd_data *) r->handle;
|
|
if (hd) {
|
|
/* Check if this function is running in the context of
|
|
* the correct httpd server thread */
|
|
if (httpd_os_thread_handle() == hd->hd_td.handle) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Helper function to get a URL query tag from a query string of the type param1=val1¶m2=val2 */
|
|
esp_err_t httpd_query_key_value(const char *qry_str, const char *key, char *val, size_t val_size)
|
|
{
|
|
if (qry_str == NULL || key == NULL || val == NULL) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
const char *qry_ptr = qry_str;
|
|
const size_t buf_len = val_size;
|
|
|
|
while (strlen(qry_ptr)) {
|
|
/* Search for the '=' character. Else, it would mean
|
|
* that the parameter is invalid */
|
|
const char *val_ptr = strchr(qry_ptr, '=');
|
|
if (!val_ptr) {
|
|
break;
|
|
}
|
|
size_t offset = val_ptr - qry_ptr;
|
|
|
|
/* If the key, does not match, continue searching.
|
|
* Compare lengths first as key from url is not
|
|
* null terminated (has '=' in the end) */
|
|
if ((offset != strlen(key)) ||
|
|
(strncasecmp(qry_ptr, key, offset))) {
|
|
/* Get the name=val string. Multiple name=value pairs
|
|
* are separated by '&' */
|
|
qry_ptr = strchr(val_ptr, '&');
|
|
if (!qry_ptr) {
|
|
break;
|
|
}
|
|
qry_ptr++;
|
|
continue;
|
|
}
|
|
|
|
/* Locate start of next query */
|
|
qry_ptr = strchr(++val_ptr, '&');
|
|
/* Or this could be the last query, in which
|
|
* case get to the end of query string */
|
|
if (!qry_ptr) {
|
|
qry_ptr = val_ptr + strlen(val_ptr);
|
|
}
|
|
|
|
/* Update value length, including one byte for null */
|
|
val_size = qry_ptr - val_ptr + 1;
|
|
|
|
/* Copy value to the caller's buffer. */
|
|
strlcpy(val, val_ptr, MIN(val_size, buf_len));
|
|
|
|
/* If buffer length is smaller than needed, return truncation error */
|
|
if (buf_len < val_size) {
|
|
return ESP_ERR_HTTPD_RESULT_TRUNC;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
ESP_LOGD(TAG, LOG_FMT("key %s not found"), key);
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
size_t httpd_req_get_url_query_len(httpd_req_t *r)
|
|
{
|
|
if (r == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
if (!httpd_valid_req(r)) {
|
|
return 0;
|
|
}
|
|
|
|
struct httpd_req_aux *ra = r->aux;
|
|
struct http_parser_url *res = &ra->url_parse_res;
|
|
|
|
/* Check if query field is present in the URL */
|
|
if (res->field_set & (1 << UF_QUERY)) {
|
|
return res->field_data[UF_QUERY].len;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
esp_err_t httpd_req_get_url_query_str(httpd_req_t *r, char *buf, size_t buf_len)
|
|
{
|
|
if (r == NULL || buf == NULL) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
if (!httpd_valid_req(r)) {
|
|
return ESP_ERR_HTTPD_INVALID_REQ;
|
|
}
|
|
|
|
struct httpd_req_aux *ra = r->aux;
|
|
struct http_parser_url *res = &ra->url_parse_res;
|
|
|
|
/* Check if query field is present in the URL */
|
|
if (res->field_set & (1 << UF_QUERY)) {
|
|
const char *qry = r->uri + res->field_data[UF_QUERY].off;
|
|
|
|
/* Minimum required buffer len for keeping
|
|
* null terminated query string */
|
|
size_t min_buf_len = res->field_data[UF_QUERY].len + 1;
|
|
|
|
strlcpy(buf, qry, MIN(buf_len, min_buf_len));
|
|
if (buf_len < min_buf_len) {
|
|
return ESP_ERR_HTTPD_RESULT_TRUNC;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
/* Get the length of the value string of a header request field */
|
|
size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *field)
|
|
{
|
|
if (r == NULL || field == NULL) {
|
|
return 0;
|
|
}
|
|
|
|
if (!httpd_valid_req(r)) {
|
|
return 0;
|
|
}
|
|
|
|
struct httpd_req_aux *ra = r->aux;
|
|
const char *hdr_ptr = ra->scratch; /*!< Request headers are kept in scratch buffer */
|
|
unsigned count = ra->req_hdrs_count; /*!< Count set during parsing */
|
|
|
|
while (count--) {
|
|
/* Search for the ':' character. Else, it would mean
|
|
* that the field is invalid
|
|
*/
|
|
const char *val_ptr = strchr(hdr_ptr, ':');
|
|
if (!val_ptr) {
|
|
break;
|
|
}
|
|
|
|
/* If the field, does not match, continue searching.
|
|
* Compare lengths first as field from header is not
|
|
* null terminated (has ':' in the end).
|
|
*/
|
|
if ((val_ptr - hdr_ptr != strlen(field)) ||
|
|
(strncasecmp(hdr_ptr, field, strlen(field)))) {
|
|
if (count) {
|
|
/* Jump to end of header field-value string */
|
|
hdr_ptr = 1 + strchr(hdr_ptr, '\0');
|
|
|
|
/* Skip all null characters (with which the line
|
|
* terminators had been overwritten) */
|
|
while (*hdr_ptr == '\0') {
|
|
hdr_ptr++;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
/* Skip ':' */
|
|
val_ptr++;
|
|
|
|
/* Skip preceding space */
|
|
while ((*val_ptr != '\0') && (*val_ptr == ' ')) {
|
|
val_ptr++;
|
|
}
|
|
return strlen(val_ptr);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Get the value of a field from the request headers */
|
|
esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *r, const char *field, char *val, size_t val_size)
|
|
{
|
|
if (r == NULL || field == NULL) {
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
if (!httpd_valid_req(r)) {
|
|
return ESP_ERR_HTTPD_INVALID_REQ;
|
|
}
|
|
|
|
struct httpd_req_aux *ra = r->aux;
|
|
const char *hdr_ptr = ra->scratch; /*!< Request headers are kept in scratch buffer */
|
|
unsigned count = ra->req_hdrs_count; /*!< Count set during parsing */
|
|
const size_t buf_len = val_size;
|
|
|
|
while (count--) {
|
|
/* Search for the ':' character. Else, it would mean
|
|
* that the field is invalid
|
|
*/
|
|
const char *val_ptr = strchr(hdr_ptr, ':');
|
|
if (!val_ptr) {
|
|
break;
|
|
}
|
|
|
|
/* If the field, does not match, continue searching.
|
|
* Compare lengths first as field from header is not
|
|
* null terminated (has ':' in the end).
|
|
*/
|
|
if ((val_ptr - hdr_ptr != strlen(field)) ||
|
|
(strncasecmp(hdr_ptr, field, strlen(field)))) {
|
|
if (count) {
|
|
/* Jump to end of header field-value string */
|
|
hdr_ptr = 1 + strchr(hdr_ptr, '\0');
|
|
|
|
/* Skip all null characters (with which the line
|
|
* terminators had been overwritten) */
|
|
while (*hdr_ptr == '\0') {
|
|
hdr_ptr++;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
/* Skip ':' */
|
|
val_ptr++;
|
|
|
|
/* Skip preceding space */
|
|
while ((*val_ptr != '\0') && (*val_ptr == ' ')) {
|
|
val_ptr++;
|
|
}
|
|
|
|
/* Get the NULL terminated value and copy it to the caller's buffer. */
|
|
strlcpy(val, val_ptr, buf_len);
|
|
|
|
/* Update value length, including one byte for null */
|
|
val_size = strlen(val_ptr) + 1;
|
|
|
|
/* If buffer length is smaller than needed, return truncation error */
|
|
if (buf_len < val_size) {
|
|
return ESP_ERR_HTTPD_RESULT_TRUNC;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|