diff --git a/changes-entries/h2-options-asterisk.txt b/changes-entries/h2-options-asterisk.txt new file mode 100644 index 00000000000..6dfcb7048f7 --- /dev/null +++ b/changes-entries/h2-options-asterisk.txt @@ -0,0 +1,3 @@ + *) mod_http2: Correctly handle the asterisk-form of OPTIONS requests + instead of rejecting them as malformed. PR 70195. + [Arturo Bernal ] diff --git a/modules/http2/h2_stream.c b/modules/http2/h2_stream.c index 28a4714a4de..ef5f2cad6e6 100644 --- a/modules/http2/h2_stream.c +++ b/modules/http2/h2_stream.c @@ -969,10 +969,13 @@ apr_status_t h2_stream_end_headers(h2_stream *stream, int eos, size_t raw_bytes) } } - if (req->scheme && (req->path && req->path[0] != '/')) { + if (req->scheme && (req->path && req->path[0] != '/') + && !(is_http_or_https && !strcmp(req->path, "*") + && !strcmp(req->method, "OPTIONS"))) { /* We still have a scheme, which means we need to pass an absolute URI into * our HTTP protocol handling and the missing '/' at the start will prevent - * us from doing so (as it then confuses path and authority). */ + * us from doing so (as it then confuses path and authority). + * The asterisk-form of OPTIONS is no URI path and passed as is. */ ap_log_cerror(APLOG_MARK, APLOG_INFO, 0, stream->session->c1, H2_STRM_LOG(APLOGNO(10379), stream, "Request :scheme '%s' and " "path '%s' do not allow creating an absolute URL. Failing " diff --git a/test/modules/http2/test_203_rfc9113.py b/test/modules/http2/test_203_rfc9113.py index 27bd80b6990..f0c3fa7cc26 100644 --- a/test/modules/http2/test_203_rfc9113.py +++ b/test/modules/http2/test_203_rfc9113.py @@ -71,3 +71,28 @@ def test_h2_203_02(self, env, hvalue, expvalue, status, lognos: Optional[List[st if lognos is not None: env.httpd_error_log.ignore_recent(lognos = lognos) + # RFC 9113, ch. 8.3.1: an OPTIONS request in asterisk form carries ':path: *' + def test_h2_203_03_options_asterisk(self, env): + url = env.mkurl("https", "test1", "/") + r = env.curl_get(url, options=['--http2', '-X', 'OPTIONS', + '--request-target', '*']) + assert r.exit_code == 0, f'curl output: {r.stderr}' + assert r.response["protocol"] == "HTTP/2", f'curl output: {r.stdout}' + assert r.response["status"] == 200, f'curl output: {r.stdout}' + + # the asterisk form is only valid for OPTIONS, other ':path' values + # not starting with '/' remain malformed. nghttp2 may already reset + # the stream (curl fails), otherwise we answer with a 400. + @pytest.mark.parametrize(["method", "target"], [ + ['GET', '*'], + ['POST', '*'], + ['OPTIONS', '**'], + ['OPTIONS', 'index.html'], + ]) + def test_h2_203_04_invalid_path(self, env, method, target): + url = env.mkurl("https", "test1", "/") + r = env.curl_get(url, options=['--http2', '-X', method, + '--request-target', target]) + assert r.exit_code != 0 or r.response["status"] == 400, \ + f'curl exit: {r.exit_code}, output: {r.stdout}' +