sync with OpenBSD -current
This commit is contained in:
parent
388947454d
commit
8b84d503c1
17 changed files with 446 additions and 330 deletions
|
@ -1,4 +1,4 @@
|
|||
/* $OpenBSD: relay_http.c,v 1.84 2022/12/28 21:38:29 jmc Exp $ */
|
||||
/* $OpenBSD: relay_http.c,v 1.86 2023/11/29 15:35:07 millert Exp $ */
|
||||
|
||||
/*
|
||||
* Copyright (c) 2006 - 2016 Reyk Floeter <reyk@openbsd.org>
|
||||
|
@ -161,6 +161,20 @@ relay_httpdesc_free(struct http_descriptor *desc)
|
|||
desc->http_lastheader = NULL;
|
||||
}
|
||||
|
||||
static int
|
||||
relay_http_header_name_valid(const char *name)
|
||||
{
|
||||
/*
|
||||
* RFC 9110 specifies that only the following characters are
|
||||
* permitted within HTTP header field names.
|
||||
*/
|
||||
const char token_chars[] = "!#$%&'*+-.^_`|~0123456789"
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
const size_t len = strspn(name, token_chars);
|
||||
|
||||
return (name[len] == '\0');
|
||||
}
|
||||
|
||||
void
|
||||
relay_read_http(struct bufferevent *bev, void *arg)
|
||||
{
|
||||
|
@ -196,10 +210,15 @@ relay_read_http(struct bufferevent *bev, void *arg)
|
|||
goto done;
|
||||
}
|
||||
|
||||
while (!cre->done) {
|
||||
for (;;) {
|
||||
line = evbuffer_readln(src, &linelen, EVBUFFER_EOL_CRLF);
|
||||
if (line == NULL)
|
||||
if (line == NULL) {
|
||||
/*
|
||||
* We do not process the last header on premature
|
||||
* EOF as it may not be complete.
|
||||
*/
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* An empty line indicates the end of the request.
|
||||
|
@ -208,35 +227,168 @@ relay_read_http(struct bufferevent *bev, void *arg)
|
|||
if (linelen == 0) {
|
||||
cre->done = 1;
|
||||
free(line);
|
||||
line = NULL;
|
||||
if (cre->line > 1) {
|
||||
/* Process last (complete) header line. */
|
||||
goto last_header;
|
||||
}
|
||||
break;
|
||||
}
|
||||
key = line;
|
||||
|
||||
/* Limit the total header length minus \r\n */
|
||||
cre->headerlen += linelen;
|
||||
if (cre->headerlen > proto->httpheaderlen) {
|
||||
free(line);
|
||||
relay_abort_http(con, 413,
|
||||
"request headers too large", 0);
|
||||
return;
|
||||
goto abort;
|
||||
}
|
||||
|
||||
/* Reject requests with an embedded NUL byte. */
|
||||
if (memchr(line, '\0', linelen) != NULL) {
|
||||
relay_abort_http(con, 400, "malformed", 0);
|
||||
goto abort;
|
||||
}
|
||||
|
||||
hs = con->se_priv;
|
||||
DPRINTF("%s: session %d http_session %p", __func__,
|
||||
con->se_id, hs);
|
||||
|
||||
/*
|
||||
* The first line is the GET/POST/PUT/... request,
|
||||
* subsequent lines are HTTP headers.
|
||||
*/
|
||||
if (++cre->line == 1)
|
||||
value = strchr(key, ' ');
|
||||
else if (*key == ' ' || *key == '\t')
|
||||
/* Multiline headers wrap with a space or tab */
|
||||
value = NULL;
|
||||
else
|
||||
value = strchr(key, ':');
|
||||
if (value == NULL) {
|
||||
if (cre->line <= 2) {
|
||||
free(line);
|
||||
if (++cre->line == 1) {
|
||||
key = line;
|
||||
if ((value = strchr(key, ' ')) == NULL) {
|
||||
relay_abort_http(con, 400, "malformed", 0);
|
||||
return;
|
||||
goto abort;
|
||||
}
|
||||
*value++ = '\0';
|
||||
|
||||
if (cre->dir == RELAY_DIR_RESPONSE) {
|
||||
desc->http_method = HTTP_METHOD_RESPONSE;
|
||||
hmn = SIMPLEQ_FIRST(&hs->hs_methods);
|
||||
|
||||
/*
|
||||
* There is nothing preventing the relay from
|
||||
* sending an unbalanced response. Be prepared.
|
||||
*/
|
||||
if (hmn == NULL) {
|
||||
request_method = HTTP_METHOD_NONE;
|
||||
DPRINTF("%s: session %d unbalanced "
|
||||
"response", __func__, con->se_id);
|
||||
} else {
|
||||
SIMPLEQ_REMOVE_HEAD(&hs->hs_methods,
|
||||
hmn_entry);
|
||||
request_method = hmn->hmn_method;
|
||||
DPRINTF("%s: session %d dequeuing %s",
|
||||
__func__, con->se_id,
|
||||
relay_httpmethod_byid(request_method));
|
||||
free(hmn);
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode response path and query
|
||||
*/
|
||||
desc->http_version = strdup(key);
|
||||
if (desc->http_version == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
desc->http_rescode = strdup(value);
|
||||
if (desc->http_rescode == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
desc->http_resmesg = strchr(desc->http_rescode,
|
||||
' ');
|
||||
if (desc->http_resmesg == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
*desc->http_resmesg++ = '\0';
|
||||
desc->http_resmesg = strdup(desc->http_resmesg);
|
||||
if (desc->http_resmesg == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
desc->http_status = strtonum(desc->http_rescode,
|
||||
100, 599, &errstr);
|
||||
if (errstr) {
|
||||
DPRINTF(
|
||||
"%s: http_status %s: errno %d, %s",
|
||||
__func__, desc->http_rescode, errno,
|
||||
errstr);
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
DPRINTF("http_version %s http_rescode %s "
|
||||
"http_resmesg %s", desc->http_version,
|
||||
desc->http_rescode, desc->http_resmesg);
|
||||
} else if (cre->dir == RELAY_DIR_REQUEST) {
|
||||
desc->http_method =
|
||||
relay_httpmethod_byname(key);
|
||||
if (desc->http_method == HTTP_METHOD_NONE) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
if ((hmn = calloc(1, sizeof *hmn)) == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
hmn->hmn_method = desc->http_method;
|
||||
DPRINTF("%s: session %d enqueuing %s",
|
||||
__func__, con->se_id,
|
||||
relay_httpmethod_byid(hmn->hmn_method));
|
||||
SIMPLEQ_INSERT_TAIL(&hs->hs_methods, hmn,
|
||||
hmn_entry);
|
||||
/*
|
||||
* Decode request path and query
|
||||
*/
|
||||
desc->http_path = strdup(value);
|
||||
if (desc->http_path == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
desc->http_version = strchr(desc->http_path,
|
||||
' ');
|
||||
if (desc->http_version == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
*desc->http_version++ = '\0';
|
||||
desc->http_query = strchr(desc->http_path, '?');
|
||||
if (desc->http_query != NULL)
|
||||
*desc->http_query++ = '\0';
|
||||
|
||||
/*
|
||||
* Have to allocate the strings because they
|
||||
* could be changed independently by the
|
||||
* filters later.
|
||||
*/
|
||||
if ((desc->http_version =
|
||||
strdup(desc->http_version)) == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
if (desc->http_query != NULL &&
|
||||
(desc->http_query =
|
||||
strdup(desc->http_query)) == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
free(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Multiline headers wrap with a space or tab. */
|
||||
if (*line == ' ' || *line == '\t') {
|
||||
if (cre->line == 2) {
|
||||
/* First header line cannot start with space. */
|
||||
relay_abort_http(con, 400, "malformed", 0);
|
||||
goto abort;
|
||||
}
|
||||
|
||||
/* Append line to the last header, if present */
|
||||
|
@ -249,220 +401,134 @@ relay_read_http(struct bufferevent *bev, void *arg)
|
|||
free(line);
|
||||
continue;
|
||||
}
|
||||
if (*value == ':') {
|
||||
*value++ = '\0';
|
||||
value += strspn(value, " \t\r\n");
|
||||
} else {
|
||||
*value++ = '\0';
|
||||
}
|
||||
|
||||
DPRINTF("%s: session %d: header '%s: %s'", __func__,
|
||||
con->se_id, key, value);
|
||||
/* Process the last complete header line. */
|
||||
last_header:
|
||||
if (desc->http_lastheader != NULL) {
|
||||
key = desc->http_lastheader->kv_key;
|
||||
value = desc->http_lastheader->kv_value;
|
||||
|
||||
hs = con->se_priv;
|
||||
DPRINTF("%s: session %d http_session %p", __func__,
|
||||
con->se_id, hs);
|
||||
DPRINTF("%s: session %d: header '%s: %s'", __func__,
|
||||
con->se_id, key, value);
|
||||
|
||||
/*
|
||||
* Identify and handle specific HTTP request methods
|
||||
*/
|
||||
if (cre->line == 1 && cre->dir == RELAY_DIR_RESPONSE) {
|
||||
desc->http_method = HTTP_METHOD_RESPONSE;
|
||||
hmn = SIMPLEQ_FIRST(&hs->hs_methods);
|
||||
|
||||
/*
|
||||
* There is nothing preventing the relay to send
|
||||
* an unbalanced response. Be prepared.
|
||||
*/
|
||||
if (hmn == NULL) {
|
||||
request_method = HTTP_METHOD_NONE;
|
||||
DPRINTF("%s: session %d unbalanced response",
|
||||
__func__, con->se_id);
|
||||
} else {
|
||||
SIMPLEQ_REMOVE_HEAD(&hs->hs_methods, hmn_entry);
|
||||
request_method = hmn->hmn_method;
|
||||
DPRINTF("%s: session %d dequeuing %s", __func__,
|
||||
con->se_id,
|
||||
relay_httpmethod_byid(request_method));
|
||||
free(hmn);
|
||||
}
|
||||
|
||||
/*
|
||||
* Decode response path and query
|
||||
*/
|
||||
desc->http_version = strdup(line);
|
||||
if (desc->http_version == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
desc->http_rescode = strdup(value);
|
||||
if (desc->http_rescode == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
desc->http_resmesg = strchr(desc->http_rescode, ' ');
|
||||
if (desc->http_resmesg == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
*desc->http_resmesg++ = '\0';
|
||||
if ((desc->http_resmesg = strdup(desc->http_resmesg))
|
||||
== NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
desc->http_status = strtonum(desc->http_rescode, 100,
|
||||
599, &errstr);
|
||||
if (errstr) {
|
||||
DPRINTF("%s: http_status %s: errno %d, %s",
|
||||
__func__, desc->http_rescode, errno,
|
||||
errstr);
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
DPRINTF("http_version %s http_rescode %s "
|
||||
"http_resmesg %s", desc->http_version,
|
||||
desc->http_rescode, desc->http_resmesg);
|
||||
goto lookup;
|
||||
} else if (cre->line == 1 && cre->dir == RELAY_DIR_REQUEST) {
|
||||
if ((desc->http_method = relay_httpmethod_byname(key))
|
||||
== HTTP_METHOD_NONE) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
if ((hmn = calloc(1, sizeof *hmn)) == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
hmn->hmn_method = desc->http_method;
|
||||
DPRINTF("%s: session %d enqueuing %s", __func__,
|
||||
con->se_id,
|
||||
relay_httpmethod_byid(hmn->hmn_method));
|
||||
SIMPLEQ_INSERT_TAIL(&hs->hs_methods, hmn, hmn_entry);
|
||||
/*
|
||||
* Decode request path and query
|
||||
*/
|
||||
desc->http_path = strdup(value);
|
||||
if (desc->http_path == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
desc->http_version = strchr(desc->http_path, ' ');
|
||||
if (desc->http_version == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
*desc->http_version++ = '\0';
|
||||
desc->http_query = strchr(desc->http_path, '?');
|
||||
if (desc->http_query != NULL)
|
||||
*desc->http_query++ = '\0';
|
||||
|
||||
/*
|
||||
* Have to allocate the strings because they could
|
||||
* be changed independently by the filters later.
|
||||
*/
|
||||
if ((desc->http_version =
|
||||
strdup(desc->http_version)) == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
if (desc->http_query != NULL &&
|
||||
(desc->http_query =
|
||||
strdup(desc->http_query)) == NULL) {
|
||||
free(line);
|
||||
goto fail;
|
||||
}
|
||||
} else if (desc->http_method != HTTP_METHOD_NONE &&
|
||||
strcasecmp("Content-Length", key) == 0) {
|
||||
/*
|
||||
* These methods should not have a body
|
||||
* and thus no Content-Length header.
|
||||
*/
|
||||
if (desc->http_method == HTTP_METHOD_TRACE ||
|
||||
desc->http_method == HTTP_METHOD_CONNECT) {
|
||||
relay_abort_http(con, 400, "malformed", 0);
|
||||
goto abort;
|
||||
}
|
||||
/*
|
||||
* HEAD responses may provide a Content-Length header,
|
||||
* but if so it should just be ignored, since there is
|
||||
* no actual payload in the response.
|
||||
*/
|
||||
if (desc->http_method != HTTP_METHOD_RESPONSE
|
||||
|| request_method != HTTP_METHOD_HEAD) {
|
||||
if (desc->http_method != HTTP_METHOD_NONE &&
|
||||
strcasecmp("Content-Length", key) == 0) {
|
||||
/*
|
||||
* Need to read data from the client after the
|
||||
* HTTP header.
|
||||
* XXX What about non-standard clients not
|
||||
* using the carriage return? And some browsers
|
||||
* seem to include the line length in the
|
||||
* content-length.
|
||||
* These methods should not have a body
|
||||
* and thus no Content-Length header.
|
||||
*/
|
||||
cre->toread = strtonum(value, 0, LLONG_MAX,
|
||||
&errstr);
|
||||
if (errstr) {
|
||||
relay_abort_http(con, 500, errstr, 0);
|
||||
if (desc->http_method == HTTP_METHOD_TRACE ||
|
||||
desc->http_method == HTTP_METHOD_CONNECT) {
|
||||
relay_abort_http(con, 400, "malformed",
|
||||
0);
|
||||
goto abort;
|
||||
}
|
||||
/*
|
||||
* HEAD responses may provide a Content-Length
|
||||
* header, but if so it should just be ignored,
|
||||
* since there is no actual payload in the
|
||||
* response.
|
||||
*/
|
||||
if (desc->http_method != HTTP_METHOD_RESPONSE
|
||||
|| request_method != HTTP_METHOD_HEAD) {
|
||||
/*
|
||||
* Need to read data from the client
|
||||
* after the HTTP header.
|
||||
* XXX What about non-standard clients
|
||||
* not using the carriage return? And
|
||||
* some browsers seem to include the
|
||||
* line length in the content-length.
|
||||
*/
|
||||
if (*value == '+' || *value == '-') {
|
||||
errstr = "invalid";
|
||||
} else {
|
||||
cre->toread = strtonum(value, 0,
|
||||
LLONG_MAX, &errstr);
|
||||
}
|
||||
if (errstr) {
|
||||
relay_abort_http(con, 500,
|
||||
errstr, 0);
|
||||
goto abort;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Response with a status code of 1xx
|
||||
* (Informational) or 204 (No Content) MUST
|
||||
* not have a Content-Length (rfc 7230 3.3.3)
|
||||
* Instead we check for value != 0 because there
|
||||
* are servers that do not follow the rfc and
|
||||
* send Content-Length: 0.
|
||||
*/
|
||||
if (desc->http_method == HTTP_METHOD_RESPONSE &&
|
||||
(((desc->http_status >= 100 &&
|
||||
desc->http_status < 200) ||
|
||||
desc->http_status == 204)) &&
|
||||
cre->toread != 0) {
|
||||
relay_abort_http(con, 502,
|
||||
"Bad Gateway", 0);
|
||||
goto abort;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* response with a status code of 1xx
|
||||
* (Informational) or 204 (No Content) MUST
|
||||
* not have a Content-Length (rfc 7230 3.3.3)
|
||||
* Instead we check for value != 0 because there are
|
||||
* servers that do not follow the rfc and send
|
||||
* Content-Length: 0.
|
||||
*/
|
||||
if (desc->http_method == HTTP_METHOD_RESPONSE && (
|
||||
((desc->http_status >= 100 &&
|
||||
desc->http_status < 200) ||
|
||||
desc->http_status == 204)) &&
|
||||
cre->toread != 0) {
|
||||
relay_abort_http(con, 502,
|
||||
"Bad Gateway", 0);
|
||||
goto abort;
|
||||
}
|
||||
}
|
||||
lookup:
|
||||
if (strcasecmp("Transfer-Encoding", key) == 0 &&
|
||||
strcasecmp("chunked", value) == 0)
|
||||
desc->http_chunked = 1;
|
||||
|
||||
/* The following header should only occur once */
|
||||
if (strcasecmp("Host", key) == 0) {
|
||||
unique = 1;
|
||||
|
||||
/*
|
||||
* The path may contain a URL. The host in the
|
||||
* URL has to match the Host: value.
|
||||
*/
|
||||
if (parse_url(desc->http_path,
|
||||
&urlproto, &host, &path) == 0) {
|
||||
ret = strcasecmp(host, value);
|
||||
free(urlproto);
|
||||
free(host);
|
||||
free(path);
|
||||
if (ret != 0) {
|
||||
if (strcasecmp("Transfer-Encoding", key) == 0) {
|
||||
/* We don't support other encodings. */
|
||||
if (strcasecmp("chunked", value) != 0) {
|
||||
relay_abort_http(con, 400,
|
||||
"malformed host", 0);
|
||||
"malformed", 0);
|
||||
goto abort;
|
||||
}
|
||||
desc->http_chunked = 1;
|
||||
}
|
||||
} else
|
||||
unique = 0;
|
||||
|
||||
if (cre->line != 1) {
|
||||
if ((hdr = kv_add(&desc->http_headers, key,
|
||||
value, unique)) == NULL) {
|
||||
relay_abort_http(con, 400,
|
||||
"malformed header", 0);
|
||||
goto abort;
|
||||
if (strcasecmp("Host", key) == 0) {
|
||||
/*
|
||||
* The path may contain a URL. The host in the
|
||||
* URL has to match the Host: value.
|
||||
*/
|
||||
if (parse_url(desc->http_path,
|
||||
&urlproto, &host, &path) == 0) {
|
||||
ret = strcasecmp(host, value);
|
||||
free(urlproto);
|
||||
free(host);
|
||||
free(path);
|
||||
if (ret != 0) {
|
||||
relay_abort_http(con, 400,
|
||||
"malformed host", 0);
|
||||
goto abort;
|
||||
}
|
||||
}
|
||||
}
|
||||
desc->http_lastheader = hdr;
|
||||
}
|
||||
|
||||
if (cre->done)
|
||||
break;
|
||||
|
||||
/* Validate header field name and check for missing value. */
|
||||
key = line;
|
||||
if ((value = strchr(line, ':')) == NULL) {
|
||||
relay_abort_http(con, 400, "malformed", 0);
|
||||
goto abort;
|
||||
}
|
||||
*value++ = '\0';
|
||||
value += strspn(value, " \t\r\n");
|
||||
|
||||
if (!relay_http_header_name_valid(key)) {
|
||||
relay_abort_http(con, 400, "malformed", 0);
|
||||
goto abort;
|
||||
}
|
||||
|
||||
/* The "Host" header must only occur once. */
|
||||
unique = strcasecmp("Host", key) == 0;
|
||||
|
||||
if ((hdr = kv_add(&desc->http_headers, key,
|
||||
value, unique)) == NULL) {
|
||||
relay_abort_http(con, 400, "malformed header", 0);
|
||||
goto abort;
|
||||
}
|
||||
desc->http_lastheader = hdr;
|
||||
|
||||
free(line);
|
||||
}
|
||||
|
||||
if (cre->done) {
|
||||
if (desc->http_method == HTTP_METHOD_NONE) {
|
||||
relay_abort_http(con, 406, "no method", 0);
|
||||
|
@ -721,7 +787,7 @@ relay_read_httpchunks(struct bufferevent *bev, void *arg)
|
|||
struct rsession *con = cre->con;
|
||||
struct protocol *proto = con->se_relay->rl_proto;
|
||||
struct evbuffer *src = EVBUFFER_INPUT(bev);
|
||||
char *line;
|
||||
char *line, *ep;
|
||||
long long llval;
|
||||
size_t size, linelen;
|
||||
|
||||
|
@ -766,10 +832,19 @@ relay_read_httpchunks(struct bufferevent *bev, void *arg)
|
|||
}
|
||||
|
||||
/*
|
||||
* Read prepended chunk size in hex, ignore the trailer.
|
||||
* Read prepended chunk size in hex without leading +0[Xx].
|
||||
* The returned signed value must not be negative.
|
||||
*/
|
||||
if (sscanf(line, "%llx", &llval) != 1 || llval < 0) {
|
||||
if (line[0] == '+' || line[0] == '-' ||
|
||||
(line[0] == '0' && (line[1] == 'x' || line[1] == 'X'))) {
|
||||
/* Reject values like 0xdead and 0XBEEF or +FEED. */
|
||||
ep = line;
|
||||
} else {
|
||||
errno = 0;
|
||||
llval = strtoll(line, &ep, 16);
|
||||
}
|
||||
if (ep == line || *ep != '\0' || llval < 0 ||
|
||||
(errno == ERANGE && llval == LLONG_MAX)) {
|
||||
free(line);
|
||||
relay_close(con, "invalid chunk size", 1);
|
||||
return;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue