diff options
Diffstat (limited to 'modules/http2/h2_request.c')
-rw-r--r-- | modules/http2/h2_request.c | 494 |
1 files changed, 374 insertions, 120 deletions
diff --git a/modules/http2/h2_request.c b/modules/http2/h2_request.c index 8899c4f..2713947 100644 --- a/modules/http2/h2_request.c +++ b/modules/http2/h2_request.c @@ -16,7 +16,12 @@ #include <assert.h> -#include <apr_strings.h> +#include "apr.h" +#include "apr_strings.h" +#include "apr_lib.h" +#include "apr_strmatch.h" + +#include <ap_mmn.h> #include <httpd.h> #include <http_core.h> @@ -24,6 +29,7 @@ #include <http_protocol.h> #include <http_request.h> #include <http_log.h> +#include <http_ssl.h> #include <http_vhost.h> #include <util_filter.h> #include <ap_mpm.h> @@ -32,11 +38,28 @@ #include "h2_private.h" #include "h2_config.h" +#include "h2_conn_ctx.h" #include "h2_push.h" #include "h2_request.h" #include "h2_util.h" +h2_request *h2_request_create(int id, apr_pool_t *pool, const char *method, + const char *scheme, const char *authority, + const char *path, apr_table_t *header) +{ + h2_request *req = apr_pcalloc(pool, sizeof(h2_request)); + + req->method = method; + req->scheme = scheme; + req->authority = authority; + req->path = path; + req->headers = header? header : apr_table_make(pool, 10); + req->request_time = apr_time_now(); + + return req; +} + typedef struct { apr_table_t *headers; apr_pool_t *pool; @@ -46,9 +69,9 @@ typedef struct { static int set_h1_header(void *ctx, const char *key, const char *value) { h1_ctx *x = ctx; - x->status = h2_req_add_header(x->headers, x->pool, key, strlen(key), - value, strlen(value)); - return (x->status == APR_SUCCESS)? 1 : 0; + int was_added; + h2_req_add_header(x->headers, x->pool, key, strlen(key), value, strlen(value), 0, &was_added); + return 1; } apr_status_t h2_request_rcreate(h2_request **preq, apr_pool_t *pool, @@ -68,54 +91,67 @@ apr_status_t h2_request_rcreate(h2_request **preq, apr_pool_t *pool, return APR_EINVAL; } - if (!ap_strchr_c(authority, ':') && r->server && r->server->port) { - apr_port_t defport = apr_uri_port_of_scheme(scheme); - if (defport != r->server->port) { - /* port info missing and port is not default for scheme: append */ - authority = apr_psprintf(pool, "%s:%d", authority, - (int)r->server->port); + /* The authority we carry in h2_request is the 'authority' part of + * the URL for the request. r->hostname has stripped any port info that + * might have been present. Do we need to add it? + */ + if (!ap_strchr_c(authority, ':')) { + if (r->parsed_uri.port_str) { + /* Yes, it was there, add it again. */ + authority = apr_pstrcat(pool, authority, ":", r->parsed_uri.port_str, NULL); + } + else if (!r->parsed_uri.hostname && r->server && r->server->port) { + /* If there was no hostname in the parsed URL, the URL was relative. + * In that case, we restore port from our server->port, if it + * is known and not the default port for the scheme. */ + apr_port_t defport = apr_uri_port_of_scheme(scheme); + if (defport != r->server->port) { + /* port info missing and port is not default for scheme: append */ + authority = apr_psprintf(pool, "%s:%d", authority, + (int)r->server->port); + } } } - + req = apr_pcalloc(pool, sizeof(*req)); - req->method = apr_pstrdup(pool, r->method); - req->scheme = scheme; - req->authority = authority; - req->path = path; - req->headers = apr_table_make(pool, 10); - if (r->server) { - req->serialize = h2_config_geti(h2_config_sget(r->server), - H2_CONF_SER_HEADERS); - } + req->method = apr_pstrdup(pool, r->method); + req->scheme = scheme; + req->authority = authority; + req->path = path; + req->headers = apr_table_make(pool, 10); + req->http_status = H2_HTTP_STATUS_UNSET; + req->request_time = apr_time_now(); x.pool = pool; x.headers = req->headers; x.status = APR_SUCCESS; apr_table_do(set_h1_header, &x, r->headers_in, NULL); - + *preq = req; return x.status; } -apr_status_t h2_request_add_header(h2_request *req, apr_pool_t *pool, +apr_status_t h2_request_add_header(h2_request *req, apr_pool_t *pool, const char *name, size_t nlen, - const char *value, size_t vlen) + const char *value, size_t vlen, + size_t max_field_len, int *pwas_added) { apr_status_t status = APR_SUCCESS; - + + *pwas_added = 0; if (nlen <= 0) { return status; } - + if (name[0] == ':') { /* pseudo header, see ch. 8.1.2.3, always should come first */ if (!apr_is_empty_table(req->headers)) { ap_log_perror(APLOG_MARK, APLOG_ERR, 0, pool, - APLOGNO(02917) + APLOGNO(02917) "h2_request: pseudo header after request start"); return APR_EGENERAL; } - + if (H2_HEADER_METHOD_LEN == nlen && !strncmp(H2_HEADER_METHOD, name, nlen)) { req->method = apr_pstrndup(pool, value, vlen); @@ -132,32 +168,36 @@ apr_status_t h2_request_add_header(h2_request *req, apr_pool_t *pool, && !strncmp(H2_HEADER_AUTH, name, nlen)) { req->authority = apr_pstrndup(pool, value, vlen); } + else if (H2_HEADER_PROTO_LEN == nlen + && !strncmp(H2_HEADER_PROTO, name, nlen)) { + req->protocol = apr_pstrndup(pool, value, vlen); + } else { char buffer[32]; memset(buffer, 0, 32); strncpy(buffer, name, (nlen > 31)? 31 : nlen); ap_log_perror(APLOG_MARK, APLOG_WARNING, 0, pool, - APLOGNO(02954) + APLOGNO(02954) "h2_request: ignoring unknown pseudo header %s", buffer); } } else { - /* non-pseudo header, append to work bucket of stream */ - status = h2_req_add_header(req->headers, pool, name, nlen, value, vlen); + /* non-pseudo header, add to table */ + status = h2_req_add_header(req->headers, pool, name, nlen, value, vlen, + max_field_len, pwas_added); } - + return status; } -apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, int eos, size_t raw_bytes) +apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, + size_t raw_bytes) { - const char *s; - - /* rfc7540, ch. 8.1.2.3: - * - if we have :authority, it overrides any Host header - * - :authority MUST be ommited when converting h1->h2, so we - * might get a stream without, but then Host needs to be there */ + /* rfc7540, ch. 8.1.2.3: without :authority, Host: must be there */ + if (req->authority && !strlen(req->authority)) { + req->authority = NULL; + } if (!req->authority) { const char *host = apr_table_get(req->headers, "Host"); if (!host) { @@ -168,30 +208,8 @@ apr_status_t h2_request_end_headers(h2_request *req, apr_pool_t *pool, int eos, else { apr_table_setn(req->headers, "Host", req->authority); } - - s = apr_table_get(req->headers, "Content-Length"); - if (!s) { - /* HTTP/2 does not need a Content-Length for framing, but our - * internal request processing is used to HTTP/1.1, so we - * need to either add a Content-Length or a Transfer-Encoding - * if any content can be expected. */ - if (!eos) { - /* We have not seen a content-length and have no eos, - * simulate a chunked encoding for our HTTP/1.1 infrastructure, - * in case we have "H2SerializeHeaders on" here - */ - req->chunked = 1; - apr_table_mergen(req->headers, "Transfer-Encoding", "chunked"); - } - else if (apr_table_get(req->headers, "Content-Type")) { - /* If we have a content-type, but already seen eos, no more - * data will come. Signal a zero content length explicitly. - */ - apr_table_setn(req->headers, "Content-Length", "0"); - } - } req->raw_bytes += raw_bytes; - + return APR_SUCCESS; } @@ -202,17 +220,16 @@ h2_request *h2_request_clone(apr_pool_t *p, const h2_request *src) dst->scheme = apr_pstrdup(p, src->scheme); dst->authority = apr_pstrdup(p, src->authority); dst->path = apr_pstrdup(p, src->path); + dst->protocol = apr_pstrdup(p, src->protocol); dst->headers = apr_table_clone(p, src->headers); return dst; } -request_rec *h2_request_create_rec(const h2_request *req, conn_rec *c) +#if !AP_MODULE_MAGIC_AT_LEAST(20120211, 106) +static request_rec *my_ap_create_request(conn_rec *c) { - int access_status = HTTP_OK; - const char *rpath; apr_pool_t *p; request_rec *r; - const char *s; apr_pool_create(&p, c->pool); apr_pool_tag(p, "request"); @@ -221,86 +238,294 @@ request_rec *h2_request_create_rec(const h2_request *req, conn_rec *c) r->pool = p; r->connection = c; r->server = c->base_server; - + r->user = NULL; r->ap_auth_type = NULL; - + r->allowed_methods = ap_make_method_list(p, 2); - - r->headers_in = apr_table_clone(r->pool, req->headers); + + r->headers_in = apr_table_make(r->pool, 5); r->trailers_in = apr_table_make(r->pool, 5); r->subprocess_env = apr_table_make(r->pool, 25); r->headers_out = apr_table_make(r->pool, 12); r->err_headers_out = apr_table_make(r->pool, 5); r->trailers_out = apr_table_make(r->pool, 5); r->notes = apr_table_make(r->pool, 5); - + r->request_config = ap_create_request_config(r->pool); /* Must be set before we run create request hook */ - + r->proto_output_filters = c->output_filters; r->output_filters = r->proto_output_filters; r->proto_input_filters = c->input_filters; r->input_filters = r->proto_input_filters; ap_run_create_request(r); r->per_dir_config = r->server->lookup_defaults; - + r->sent_bodyct = 0; /* bytect isn't for body */ - + r->read_length = 0; r->read_body = REQUEST_NO_BODY; - + r->status = HTTP_OK; /* Until further notice */ r->header_only = 0; r->the_request = NULL; - + /* Begin by presuming any module can make its own path_info assumptions, * until some module interjects and changes the value. */ r->used_path_info = AP_REQ_DEFAULT_PATH_INFO; - + r->useragent_addr = c->client_addr; r->useragent_ip = c->client_ip; - + return r; +} +#endif + +#if AP_HAS_RESPONSE_BUCKETS +apr_bucket *h2_request_create_bucket(const h2_request *req, request_rec *r) +{ + conn_rec *c = r->connection; + apr_table_t *headers = apr_table_clone(r->pool, req->headers); + const char *uri = req->path; + + AP_DEBUG_ASSERT(req->method); + AP_DEBUG_ASSERT(req->authority); + if (!ap_cstr_casecmp("CONNECT", req->method)) { + uri = req->authority; + } + else if (h2_config_cgeti(c, H2_CONF_PROXY_REQUESTS)) { + /* Forward proxying: always absolute uris */ + uri = apr_psprintf(r->pool, "%s://%s%s", + req->scheme, req->authority, + req->path ? req->path : ""); + } + else if (req->scheme && ap_cstr_casecmp(req->scheme, "http") + && ap_cstr_casecmp(req->scheme, "https")) { + /* Client sent a non-http ':scheme', use an absolute URI */ + uri = apr_psprintf(r->pool, "%s://%s%s", + req->scheme, req->authority, req->path ? req->path : ""); + } + + return ap_bucket_request_create(req->method, uri, "HTTP/2.0", headers, + r->pool, c->bucket_alloc); +} +#endif + +static void assign_headers(request_rec *r, const h2_request *req, + int no_body, int is_connect) +{ + const char *cl; + + r->headers_in = apr_table_clone(r->pool, req->headers); + + if (req->authority && !is_connect) { + /* for internal handling, we have to simulate that :authority + * came in as Host:, RFC 9113 ch. says that mismatches between + * :authority and Host: SHOULD be rejected as malformed. However, + * we are more lenient and just replace any Host: if we have + * an :authority. + */ + const char *orig_host = apr_table_get(req->headers, "Host"); + if (orig_host && strcmp(req->authority, orig_host)) { + ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(10401) + "overwriting 'Host: %s' with :authority: %s'", + orig_host, req->authority); + apr_table_setn(r->subprocess_env, "H2_ORIGINAL_HOST", orig_host); + } + apr_table_setn(r->headers_in, "Host", req->authority); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "set 'Host: %s' from :authority", req->authority); + } + + /* Unless we open a byte stream via CONNECT, apply content-length guards. */ + if (!is_connect) { + cl = apr_table_get(req->headers, "Content-Length"); + if (no_body) { + if (!cl && apr_table_get(req->headers, "Content-Type")) { + /* If we have a content-type, but already seen eos, no more + * data will come. Signal a zero content length explicitly. + */ + apr_table_setn(req->headers, "Content-Length", "0"); + } + } +#if !AP_HAS_RESPONSE_BUCKETS + else if (!cl) { + /* there may be a body and we have internal HTTP/1.1 processing. + * If the Content-Length is unspecified, we MUST simulate + * chunked Transfer-Encoding. + * + * HTTP/2 does not need a Content-Length for framing. Ideally + * all clients set the EOS flag on the header frame if they + * do not intent to send a body. However, forwarding proxies + * might just no know at the time and send an empty DATA + * frame with EOS much later. + */ + apr_table_mergen(r->headers_in, "Transfer-Encoding", "chunked"); + } +#endif /* else AP_HAS_RESPONSE_BUCKETS */ + } +} + +request_rec *h2_create_request_rec(const h2_request *req, conn_rec *c, + int no_body) +{ + int access_status = HTTP_OK; + int is_connect = !ap_cstr_casecmp("CONNECT", req->method); + +#if AP_MODULE_MAGIC_AT_LEAST(20120211, 106) + request_rec *r = ap_create_request(c); +#else + request_rec *r = my_ap_create_request(c); +#endif + +#if AP_MODULE_MAGIC_AT_LEAST(20120211, 107) + assign_headers(r, req, no_body, is_connect); ap_run_pre_read_request(r, c); - + /* Time to populate r with the data we have. */ r->request_time = req->request_time; - r->method = req->method; - /* Provide quick information about the request method as soon as known */ - r->method_number = ap_method_number_of(r->method); - if (r->method_number == M_GET && r->method[0] == 'H') { - r->header_only = 1; + AP_DEBUG_ASSERT(req->authority); + if (req->http_status != H2_HTTP_STATUS_UNSET) { + access_status = req->http_status; + goto die; + } + else if (is_connect) { + /* CONNECT MUST NOT have scheme or path */ + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", + req->method, req->authority); + if (req->scheme) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10458) + "':scheme: %s' header present in CONNECT request", + req->scheme); + access_status = HTTP_BAD_REQUEST; + goto die; + } + else if (req->path) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10459) + "':path: %s' header present in CONNECT request", + req->path); + access_status = HTTP_BAD_REQUEST; + goto die; + } + } + else if (req->protocol) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10470) + "':protocol: %s' header present in %s request", + req->protocol, req->method); + access_status = HTTP_BAD_REQUEST; + goto die; + } + else if (h2_config_cgeti(c, H2_CONF_PROXY_REQUESTS)) { + if (!req->scheme) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10468) + "H2ProxyRequests on, but request misses :scheme"); + access_status = HTTP_BAD_REQUEST; + goto die; + } + if (!req->authority) { + ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(10469) + "H2ProxyRequests on, but request misses :authority"); + access_status = HTTP_BAD_REQUEST; + goto die; + } + r->the_request = apr_psprintf(r->pool, "%s %s://%s%s HTTP/2.0", + req->method, req->scheme, req->authority, + req->path ? req->path : ""); + } + else if (req->scheme && ap_cstr_casecmp(req->scheme, "http") + && ap_cstr_casecmp(req->scheme, "https")) { + /* Client sent a ':scheme' pseudo header for something else + * than what we have on this connection. Make an absolute URI. */ + r->the_request = apr_psprintf(r->pool, "%s %s://%s%s HTTP/2.0", + req->method, req->scheme, req->authority, + req->path ? req->path : ""); + } + else if (req->path) { + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", + req->method, req->path); + } + else { + /* We should only come here on a request that is errored already. + * create a request line that passes parsing, we'll die anyway. + */ + AP_DEBUG_ASSERT(req->http_status != H2_HTTP_STATUS_UNSET); + r->the_request = apr_psprintf(r->pool, "%s / HTTP/2.0", req->method); } - rpath = (req->path ? req->path : ""); - ap_parse_uri(r, rpath); - r->protocol = (char*)"HTTP/2.0"; - r->proto_num = HTTP_VERSION(2, 0); - - r->the_request = apr_psprintf(r->pool, "%s %s %s", - r->method, rpath, r->protocol); - - /* update what we think the virtual host is based on the headers we've - * now read. may update status. - * Leave r->hostname empty, vhost will parse if form our Host: header, - * otherwise we get complains about port numbers. + /* Start with r->hostname = NULL, ap_check_request_header() will get it + * form Host: header, otherwise we get complains about port numbers. */ r->hostname = NULL; - ap_update_vhost_from_headers(r); - - /* we may have switched to another server */ - r->per_dir_config = r->server->lookup_defaults; - - s = apr_table_get(r->headers_in, "Expect"); - if (s && s[0]) { - if (ap_cstr_casecmp(s, "100-continue") == 0) { - r->expecting_100 = 1; + + /* Validate HTTP/1 request and select vhost. */ + if (!ap_parse_request_line(r) || !ap_check_request_header(r)) { + /* we may have switched to another server still */ + r->per_dir_config = r->server->lookup_defaults; + if (req->http_status != H2_HTTP_STATUS_UNSET) { + access_status = req->http_status; + /* Be safe and close the connection */ + c->keepalive = AP_CONN_CLOSE; } else { - r->status = HTTP_EXPECTATION_FAILED; - ap_send_error_response(r, 0); + access_status = r->status; + } + r->status = HTTP_OK; + goto die; + } +#else + { + const char *s; + + assign_headers(r, req, no_body, is_connect); + ap_run_pre_read_request(r, c); + + /* Time to populate r with the data we have. */ + r->request_time = req->request_time; + r->method = apr_pstrdup(r->pool, req->method); + /* Provide quick information about the request method as soon as known */ + r->method_number = ap_method_number_of(r->method); + if (r->method_number == M_GET && r->method[0] == 'H') { + r->header_only = 1; } + ap_parse_uri(r, req->path ? req->path : ""); + r->protocol = (char*)"HTTP/2.0"; + r->proto_num = HTTP_VERSION(2, 0); + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", + r->method, req->path ? req->path : ""); + + /* Start with r->hostname = NULL, ap_check_request_header() will get it + * form Host: header, otherwise we get complains about port numbers. + */ + r->hostname = NULL; + ap_update_vhost_from_headers(r); + + /* we may have switched to another server */ + r->per_dir_config = r->server->lookup_defaults; + + s = apr_table_get(r->headers_in, "Expect"); + if (s && s[0]) { + if (ap_cstr_casecmp(s, "100-continue") == 0) { + r->expecting_100 = 1; + } + else { + r->status = HTTP_EXPECTATION_FAILED; + access_status = r->status; + goto die; + } + } + } +#endif + + /* we may have switched to another server */ + r->per_dir_config = r->server->lookup_defaults; + + if (req->http_status != H2_HTTP_STATUS_UNSET) { + access_status = req->http_status; + r->status = HTTP_OK; + /* Be safe and close the connection */ + c->keepalive = AP_CONN_CLOSE; + goto die; } /* @@ -311,29 +536,58 @@ request_rec *h2_request_create_rec(const h2_request *req, conn_rec *c) */ ap_add_input_filter_handle(ap_http_input_filter_handle, NULL, r, r->connection); - - if (access_status != HTTP_OK - || (access_status = ap_run_post_read_request(r))) { + + if ((access_status = ap_post_read_request(r))) { /* Request check post hooks failed. An example of this would be a * request for a vhost where h2 is disabled --> 421. */ ap_log_cerror(APLOG_MARK, APLOG_DEBUG, 0, c, APLOGNO(03367) "h2_request: access_status=%d, request_create failed", access_status); - ap_die(access_status, r); - ap_update_child_status(c->sbh, SERVER_BUSY_LOG, r); - ap_run_log_transaction(r); - r = NULL; - goto traceout; + goto die; } - AP_READ_REQUEST_SUCCESS((uintptr_t)r, (char *)r->method, - (char *)r->uri, (char *)r->server->defn_name, + AP_READ_REQUEST_SUCCESS((uintptr_t)r, (char *)r->method, + (char *)r->uri, (char *)r->server->defn_name, r->status); return r; -traceout: - AP_READ_REQUEST_FAILURE((uintptr_t)r); - return r; -} +die: + if (!r->method) { + /* if we fail early, `r` is not properly initialized for error + * processing which accesses fields in message generation. + * Make a best effort. */ + if (!r->the_request) { + r->the_request = apr_psprintf(r->pool, "%s %s HTTP/2.0", + req->method, req->path); + } + ap_parse_request_line(r); + } + ap_log_cerror(APLOG_MARK, APLOG_TRACE2, 0, c, + "ap_die(%d) for %s", access_status, r->the_request); + ap_die(access_status, r); + /* ap_die() sent the response through the output filters, we must now + * end the request with an EOR bucket for stream/pipeline accounting. + */ + { + apr_bucket_brigade *eor_bb; +#if AP_MODULE_MAGIC_AT_LEAST(20180905, 1) + eor_bb = ap_acquire_brigade(c); + APR_BRIGADE_INSERT_TAIL(eor_bb, + ap_bucket_eor_create(c->bucket_alloc, r)); + ap_pass_brigade(c->output_filters, eor_bb); + ap_release_brigade(c, eor_bb); +#else + eor_bb = apr_brigade_create(c->pool, c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(eor_bb, + ap_bucket_eor_create(c->bucket_alloc, r)); + ap_pass_brigade(c->output_filters, eor_bb); + apr_brigade_destroy(eor_bb); +#endif + } + + r = NULL; + AP_READ_REQUEST_FAILURE((uintptr_t)r); + return NULL; +} |