diff options
-rw-r--r-- | src/request.c | 144 | ||||
-rw-r--r-- | src/t/test_request.c | 26 | ||||
-rwxr-xr-x | tests/mod-fastcgi.t | 18 |
3 files changed, 102 insertions, 86 deletions
diff --git a/src/request.c b/src/request.c index fc66ac9e..12932af5 100644 --- a/src/request.c +++ b/src/request.c @@ -374,23 +374,13 @@ static int http_request_header_char_invalid(connection *con, char ch, const char return 400; } -enum keep_alive_set { - HTTP_CONNECTION_UNSET, - HTTP_CONNECTION_KEEPALIVE, - HTTP_CONNECTION_CLOSE, -}; - typedef struct { - enum keep_alive_set keep_alive_set; - char con_length_set; char *reqline_host; int reqline_hostlen; size_t reqline_len; } parse_header_state; static void init_parse_header_state(parse_header_state* state) { - state->keep_alive_set = HTTP_CONNECTION_UNSET; - state->con_length_set = 0; state->reqline_host = NULL; state->reqline_hostlen = 0; state->reqline_len = 0; @@ -402,7 +392,7 @@ static void init_parse_header_state(parse_header_state* state) { * * returns 0 on success, HTTP status on error */ -static int parse_single_header(connection *con, parse_header_state *state, const enum http_header_e id, char *k, size_t klen, char *v, size_t vlen) { +static int http_request_parse_single_header(connection *con, const enum http_header_e id, const char *k, size_t klen, const char *v, size_t vlen) { buffer **saveb = NULL; /* @@ -420,8 +410,9 @@ static int parse_single_header(connection *con, parse_header_state *state, const return http_request_header_line_invalid(con, 400, "uri-authority too long -> 400"); } } - else if (state->reqline_host) { - /* ignore all Host: headers as we got Host in request line */ + else if (NULL != con->request.http_host + && buffer_is_equal_string(con->request.http_host, v, vlen)) { + /* ignore all Host: headers if match authority in request line */ return 0; /* ignore header */ } else { @@ -432,11 +423,11 @@ static int parse_single_header(connection *con, parse_header_state *state, const /* "Connection: close" is common case if header is present */ if ((vlen == 5 && buffer_eq_icase_ssn(v, CONST_STR_LEN("close"))) || http_header_str_contains_token(v,vlen,CONST_STR_LEN("close"))) { - state->keep_alive_set = HTTP_CONNECTION_CLOSE; + con->keep_alive = 0; break; } if (http_header_str_contains_token(v,vlen,CONST_STR_LEN("keep-alive"))){ - state->keep_alive_set = HTTP_CONNECTION_KEEPALIVE; + con->keep_alive = 1; break; } break; @@ -484,6 +475,21 @@ static int parse_single_header(connection *con, parse_header_state *state, const } } break; + case HTTP_HEADER_TRANSFER_ENCODING: + if (HTTP_VERSION_1_0 == con->request.http_version) { + return http_request_header_line_invalid(con, 400, "HTTP/1.0 with Transfer-Encoding (bad HTTP/1.0 proxy?) -> 400"); + } + + if (!buffer_eq_icase_ss(v, vlen, CONST_STR_LEN("chunked"))) { + /* Transfer-Encoding might contain additional encodings, + * which are not currently supported by lighttpd */ + return http_request_header_line_invalid(con, 501, NULL); /* Not Implemented */ + } + con->request.content_length = -1; + + /* Transfer-Encoding is a hop-by-hop header, + * which must not be blindly forwarded to backends */ + return 0; /* skip header */ } http_header_request_append(con, id, k, klen, v, vlen); @@ -558,6 +564,8 @@ static size_t http_request_parse_reqline(connection *con, buffer *hdrs, parse_he if (proto[0]=='H' && proto[1]=='T' && proto[2]=='T' && proto[3]=='P' && proto[4] == '/') { if (proto[5] == '1' && proto[6] == '.' && (proto[7] == '1' || proto[7] == '0')) { con->request.http_version = (proto[7] == '1') ? HTTP_VERSION_1_1 : HTTP_VERSION_1_0; + /* keep-alive default: HTTP/1.1 -> true; HTTP/1.0 -> false */ + con->keep_alive = (HTTP_VERSION_1_0 != con->request.http_version); } else { return http_request_header_line_invalid(con, 505, "unknown HTTP version -> 505"); } @@ -615,6 +623,8 @@ static size_t http_request_parse_reqline(connection *con, buffer *hdrs, parse_he } http_header_request_set(con, HTTP_HEADER_HOST, CONST_STR_LEN("Host"), state->reqline_host, state->reqline_hostlen); con->request.http_host = http_header_request_get(con, HTTP_HEADER_HOST, CONST_STR_LEN("Host")); + if (buffer_string_is_empty(con->request.http_host)) + return http_request_header_line_invalid(con, 400, "empty uri-authority in request-line -> 400"); } return 0; @@ -753,7 +763,7 @@ static int http_request_parse_headers(connection *con, buffer *hdrs, parse_heade } } - int status = parse_single_header(con, state, id, k, (size_t)klen, v, (size_t)vlen); + int status = http_request_parse_single_header(con, id, k, (size_t)klen, v, (size_t)vlen); if (0 != status) return status; } @@ -774,37 +784,16 @@ int http_request_parse(connection *con, buffer *hdrs) { /* do some post-processing */ - if (con->request.http_version == HTTP_VERSION_1_1) { - if (state.keep_alive_set != HTTP_CONNECTION_CLOSE) { - /* no Connection-Header sent */ - - /* HTTP/1.1 -> keep-alive default TRUE */ - con->keep_alive = 1; - } else { - con->keep_alive = 0; - } - - /* RFC 2616, 14.23 */ - if (con->request.http_host == NULL || - buffer_string_is_empty(con->request.http_host)) { - return http_request_header_line_invalid(con, 400, "HTTP/1.1 but Host missing -> 400"); - } - } else { - if (state.keep_alive_set == HTTP_CONNECTION_KEEPALIVE) { - /* no Connection-Header sent */ - - /* HTTP/1.0 -> keep-alive default FALSE */ - con->keep_alive = 1; - } else { - con->keep_alive = 0; - } - } - - /* check hostname field if it is set */ - if (!buffer_is_empty(con->request.http_host) && - 0 != http_request_host_policy(con, con->request.http_host, con->proto)) { - return http_request_header_line_invalid(con, 400, "Invalid Hostname -> 400"); - } + /* check hostname field if it is set */ + if (con->request.http_host) { + if (0 != http_request_host_policy(con, con->request.http_host, con->proto)) + return http_request_header_line_invalid(con, 400, "Invalid Hostname -> 400"); + } + else { + /* RFC 2616, 14.23 */ + if (con->request.http_version == HTTP_VERSION_1_1) + return http_request_header_line_invalid(con, 400, "HTTP/1.1 but Host missing -> 400"); + } if (con->request.htags & HTTP_HEADER_TRANSFER_ENCODING) { buffer *vb = http_header_request_get(con, HTTP_HEADER_TRANSFER_ENCODING, CONST_STR_LEN("Transfer-Encoding")); @@ -844,32 +833,49 @@ int http_request_parse(connection *con, buffer *hdrs) { } } - state.con_length_set = 1; con->request.content_length = -1; } } - else if (con->request.htags & HTTP_HEADER_CONTENT_LENGTH) { - state.con_length_set = 1; - } - switch(con->request.http_method) { - case HTTP_METHOD_GET: - case HTTP_METHOD_HEAD: - /* content-length is forbidden for those */ - if (state.con_length_set && 0 != con->request.content_length - && !(con->conf.http_parseopts & HTTP_PARSEOPT_METHOD_GET_BODY)) { - return http_request_header_line_invalid(con, 400, "GET/HEAD with content-length -> 400"); - } - break; - case HTTP_METHOD_POST: - /* content-length is required for them */ - if (!state.con_length_set) { - return http_request_header_line_invalid(con, 411, "POST-request, but content-length missing -> 411"); - } - break; - default: - break; - } + if (0 == con->request.content_length) { + /* POST requires Content-Length (or Transfer-Encoding) + * (-1 == con->request.content_length when Transfer-Encoding: chunked)*/ + if (HTTP_METHOD_POST == con->request.http_method + && !(con->request.htags & HTTP_HEADER_CONTENT_LENGTH)) { + return http_request_header_line_invalid(con, 411, "POST-request, but content-length missing -> 411"); + } + } + else { + /* (-1 == con->request.content_length when Transfer-Encoding: chunked)*/ + if (-1 == con->request.content_length + && (con->request.htags & HTTP_HEADER_CONTENT_LENGTH)) { + /* RFC7230 Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing + * 3.3.3. Message Body Length + * [...] + * If a message is received with both a Transfer-Encoding and a + * Content-Length header field, the Transfer-Encoding overrides the + * Content-Length. Such a message might indicate an attempt to + * perform request smuggling (Section 9.5) or response splitting + * (Section 9.4) and ought to be handled as an error. A sender MUST + * remove the received Content-Length field prior to forwarding such + * a message downstream. + */ + const unsigned int http_header_strict = + (con->conf.http_parseopts & HTTP_PARSEOPT_HEADER_STRICT); + if (http_header_strict) { + return http_request_header_line_invalid(con, 400, "invalid Transfer-Encoding + Content-Length -> 400"); + } + else { + /* ignore Content-Length */ + http_header_request_unset(con, HTTP_HEADER_CONTENT_LENGTH, CONST_STR_LEN("Content-Length")); + } + } + if ((HTTP_METHOD_GET == con->request.http_method + || HTTP_METHOD_HEAD == con->request.http_method) + && !(con->conf.http_parseopts & HTTP_PARSEOPT_METHOD_GET_BODY)) { + return http_request_header_line_invalid(con, 400, "GET/HEAD with content-length -> 400"); + } + } return 0; } diff --git a/src/t/test_request.c b/src/t/test_request.c index fa2593d6..712727fe 100644 --- a/src/t/test_request.c +++ b/src/t/test_request.c @@ -435,6 +435,32 @@ static void test_request_http_request_parse(connection *con) "If-Modified-Since: \0\r\n" "\r\n")); + run_http_request_parse(con, __LINE__, 0, + "absolute-uri in request-line (without Host)", + CONST_STR_LEN("GET http://zzz.example.org/ HTTP/1.1\r\n" + "Connection: close\r\n" + "\r\n")); + ds = (data_string *) + array_get_element_klen(con->request.headers, CONST_STR_LEN("Host")); + assert(ds && buffer_is_equal_string(ds->value, CONST_STR_LEN("zzz.example.org"))); + + run_http_request_parse(con, __LINE__, 0, + "absolute-uri in request-line (with Host match)", + CONST_STR_LEN("GET http://zzz.example.org/ HTTP/1.1\r\n" + "Host: zzz.example.org\r\n" + "Connection: close\r\n" + "\r\n")); + ds = (data_string *) + array_get_element_klen(con->request.headers, CONST_STR_LEN("Host")); + assert(ds && buffer_is_equal_string(ds->value, CONST_STR_LEN("zzz.example.org"))); + + run_http_request_parse(con, __LINE__, 400, + "absolute-uri in request-line (with Host mismatch)", + CONST_STR_LEN("GET http://zzz.example.org/ HTTP/1.1\r\n" + "Host: aaa.example.org\r\n" + "Connection: close\r\n" + "\r\n")); + /* (quick check that none of above tests were left in a state * which resulted in subsequent tests returning 400 for other * reasons) */ diff --git a/tests/mod-fastcgi.t b/tests/mod-fastcgi.t index 7a00963c..4ae50d1f 100755 --- a/tests/mod-fastcgi.t +++ b/tests/mod-fastcgi.t @@ -7,7 +7,7 @@ BEGIN { } use strict; -use Test::More tests => 46; +use Test::More tests => 44; use LightyTest; my $tf = LightyTest->new(); @@ -194,22 +194,6 @@ EOF ok($tf->handle_http($t) == 0, '$_SERVER["AUTH_TYPE"]'); $t->{REQUEST} = ( <<EOF -GET /get-server-env.php?env=SERVER_NAME HTTP/1.0 -Host: zzz.example.org -EOF - ); - $t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200, 'HTTP-Content' => 'zzz.example.org' } ]; - ok($tf->handle_http($t) == 0, 'FastCGI + Host'); - - $t->{REQUEST} = ( <<EOF -GET http://zzz.example.org/get-server-env.php?env=SERVER_NAME HTTP/1.0 -Host: aaa.example.org -EOF - ); - $t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 200, 'HTTP-Content' => 'zzz.example.org' } ]; - ok($tf->handle_http($t) == 0, 'SERVER_NAME (absolute url in request line)'); - - $t->{REQUEST} = ( <<EOF GET /indexfile/ HTTP/1.0 Host: www.example.org EOF |