From ea105708c973bfbf2e3ae2881693d10c321b92c0 Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Mon, 17 Nov 2025 09:56:48 +0100 Subject: [PATCH] h2/h3: handle methods with spaces The parsing of the HTTP/1.1 formatted request into the h2/h3 header structures should detect CURLOPT_CUSTOMREQUEST methods and forward them correctly. Add test_01_20 to verify Fixes #19543 Reported-by: Omdahake on github Closes #19563 --- lib/http1.c | 23 ++++++++++++++++------- lib/http1.h | 5 +++-- lib/http2.c | 5 ++++- lib/vquic/curl_ngtcp2.c | 5 ++++- lib/vquic/curl_osslq.c | 5 ++++- lib/vquic/curl_quiche.c | 5 ++++- tests/http/test_01_basic.py | 22 ++++++++++++++++++++++ tests/unit/unit2603.c | 31 +++++++++++++++++++++++-------- 8 files changed, 80 insertions(+), 21 deletions(-) diff --git a/lib/http1.c b/lib/http1.c index 0403e95ba2..c487597e34 100644 --- a/lib/http1.c +++ b/lib/http1.c @@ -134,7 +134,9 @@ static ssize_t next_line(struct h1_req_parser *parser, } static CURLcode start_req(struct h1_req_parser *parser, - const char *scheme_default, int options) + const char *scheme_default, + const char *custom_method, + int options) { const char *p, *m, *target, *hv, *scheme, *authority, *path; size_t m_len, target_len, hv_len, scheme_len, authority_len, path_len; @@ -144,9 +146,15 @@ static CURLcode start_req(struct h1_req_parser *parser, DEBUGASSERT(!parser->req); /* line must match: "METHOD TARGET HTTP_VERSION" */ - p = memchr(parser->line, ' ', parser->line_len); - if(!p || p == parser->line) - goto out; + if(custom_method && custom_method[0] && + !strncmp(custom_method, parser->line, strlen(custom_method))) { + p = parser->line + strlen(custom_method); + } + else { + p = memchr(parser->line, ' ', parser->line_len); + if(!p || p == parser->line) + goto out; + } m = parser->line; m_len = p - parser->line; @@ -258,8 +266,9 @@ out: ssize_t Curl_h1_req_parse_read(struct h1_req_parser *parser, const char *buf, size_t buflen, - const char *scheme_default, int options, - CURLcode *err) + const char *scheme_default, + const char *custom_method, + int options, CURLcode *err) { ssize_t nread = 0, n; @@ -285,7 +294,7 @@ ssize_t Curl_h1_req_parse_read(struct h1_req_parser *parser, goto out; } else if(!parser->req) { - *err = start_req(parser, scheme_default, options); + *err = start_req(parser, scheme_default, custom_method, options); if(*err) { nread = -1; goto out; diff --git a/lib/http1.h b/lib/http1.h index b38b32f591..94b5a44e31 100644 --- a/lib/http1.h +++ b/lib/http1.h @@ -50,8 +50,9 @@ void Curl_h1_req_parse_free(struct h1_req_parser *parser); ssize_t Curl_h1_req_parse_read(struct h1_req_parser *parser, const char *buf, size_t buflen, - const char *scheme_default, int options, - CURLcode *err); + const char *scheme_default, + const char *custom_method, + int options, CURLcode *err); CURLcode Curl_h1_req_dprint(const struct httpreq *req, struct dynbuf *dbuf); diff --git a/lib/http2.c b/lib/http2.c index 2e1e5bd07e..1565e0b9ea 100644 --- a/lib/http2.c +++ b/lib/http2.c @@ -2248,7 +2248,10 @@ static CURLcode h2_submit(struct h2_stream_ctx **pstream, if(result) goto out; - rc = Curl_h1_req_parse_read(&stream->h1, buf, len, NULL, 0, &result); + rc = Curl_h1_req_parse_read(&stream->h1, buf, len, NULL, + !data->state.http_ignorecustom ? + data->set.str[STRING_CUSTOMREQUEST] : NULL, + 0, &result); if(!curlx_sztouz(rc, &nwritten)) goto out; *pnwritten = nwritten; diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c index 475060ebdd..ce5786ca83 100644 --- a/lib/vquic/curl_ngtcp2.c +++ b/lib/vquic/curl_ngtcp2.c @@ -1531,7 +1531,10 @@ static CURLcode h3_stream_open(struct Curl_cfilter *cf, goto out; } - nwritten = Curl_h1_req_parse_read(&stream->h1, buf, len, NULL, 0, &result); + nwritten = Curl_h1_req_parse_read(&stream->h1, buf, len, NULL, + !data->state.http_ignorecustom ? + data->set.str[STRING_CUSTOMREQUEST] : NULL, + 0, &result); if(nwritten < 0) goto out; *pnwritten = (size_t)nwritten; diff --git a/lib/vquic/curl_osslq.c b/lib/vquic/curl_osslq.c index 75dc5cc694..f328c08e35 100644 --- a/lib/vquic/curl_osslq.c +++ b/lib/vquic/curl_osslq.c @@ -1900,7 +1900,10 @@ static ssize_t h3_stream_open(struct Curl_cfilter *cf, goto out; } - nwritten = Curl_h1_req_parse_read(&stream->h1, buf, len, NULL, 0, err); + nwritten = Curl_h1_req_parse_read(&stream->h1, buf, len, NULL, + !data->state.http_ignorecustom ? + data->set.str[STRING_CUSTOMREQUEST] : NULL, + 0, err); if(nwritten < 0) goto out; if(!stream->h1.done) { diff --git a/lib/vquic/curl_quiche.c b/lib/vquic/curl_quiche.c index ff3e76b063..02b679ab84 100644 --- a/lib/vquic/curl_quiche.c +++ b/lib/vquic/curl_quiche.c @@ -991,7 +991,10 @@ static CURLcode h3_open_stream(struct Curl_cfilter *cf, Curl_dynhds_init(&h2_headers, 0, DYN_HTTP_REQUEST); DEBUGASSERT(stream); - nwritten = Curl_h1_req_parse_read(&stream->h1, buf, blen, NULL, 0, &result); + nwritten = Curl_h1_req_parse_read(&stream->h1, buf, blen, NULL, + !data->state.http_ignorecustom ? + data->set.str[STRING_CUSTOMREQUEST] : NULL, + 0, &result); if(nwritten < 0) goto out; if(!stream->h1.done) { diff --git a/tests/http/test_01_basic.py b/tests/http/test_01_basic.py index c734318890..aa94238c3f 100644 --- a/tests/http/test_01_basic.py +++ b/tests/http/test_01_basic.py @@ -25,6 +25,7 @@ ########################################################################### # import logging +import re import pytest from testenv import Env @@ -293,3 +294,24 @@ class TestBasic: r = curl.http_download(urls=[url1, url2], alpn_proto=proto, with_stats=True) assert len(r.stats) == 2 assert r.total_connects == 2, f'{r.dump_logs()}' + + # use a custom method containing a space + # check that h2/h3 did send that in the :method pseudo header. #19543 + @pytest.mark.skipif(condition=not Env.curl_is_verbose(), reason="needs verbosecurl") + @pytest.mark.parametrize("proto", Env.http_protos()) + def test_01_20_method_space(self, env: Env, proto, httpd): + curl = CurlClient(env=env) + method = 'IN SANE' + url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo' + r = curl.http_download(urls=[url], alpn_proto=proto, with_stats=True, + extra_args=['-X', method]) + assert len(r.stats) == 1 + if proto == 'h2' or proto == 'h3': + r.check_response(http_status=0) + re_m = re.compile(r'.*\[:method: ([^\]]+)\].*') + lines = [line for line in r.trace_lines if re_m.match(line)] + assert len(lines) == 1, f'{r.dump_logs()}' + m = re_m.match(lines[0]) + assert m.group(1) == method, f'{r.dump_logs()}' + else: + r.check_response(http_status=400) diff --git a/tests/unit/unit2603.c b/tests/unit/unit2603.c index 5915555f8b..a8ed09f1c2 100644 --- a/tests/unit/unit2603.c +++ b/tests/unit/unit2603.c @@ -51,6 +51,7 @@ static void check_eq(const char *s, const char *exp_s, const char *name) struct tcase { const char **input; const char *default_scheme; + const char *custom_method; const char *method; const char *scheme; const char *authority; @@ -74,7 +75,7 @@ static void parse_success(const struct tcase *t) buflen = strlen(buf); in_len += buflen; nread = Curl_h1_req_parse_read(&p, buf, buflen, t->default_scheme, - 0, &err); + t->custom_method, 0, &err); if(nread < 0) { curl_mfprintf(stderr, "got err %d parsing: '%s'\n", err, buf); fail("error consuming"); @@ -122,10 +123,10 @@ static CURLcode test_unit2603(const char *arg) NULL, }; static const struct tcase TEST1a = { - T1_INPUT, NULL, "GET", NULL, NULL, "/path", 1, 0 + T1_INPUT, NULL, NULL, "GET", NULL, NULL, "/path", 1, 0 }; static const struct tcase TEST1b = { - T1_INPUT, "https", "GET", "https", NULL, "/path", 1, 0 + T1_INPUT, "https", NULL, "GET", "https", NULL, "/path", 1, 0 }; static const char *T2_INPUT[] = { @@ -136,7 +137,7 @@ static CURLcode test_unit2603(const char *arg) NULL, }; static const struct tcase TEST2 = { - T2_INPUT, NULL, "GET", NULL, NULL, "/path", 1, 8 + T2_INPUT, NULL, NULL, "GET", NULL, NULL, "/path", 1, 8 }; static const char *T3_INPUT[] = { @@ -145,7 +146,7 @@ static CURLcode test_unit2603(const char *arg) NULL, }; static const struct tcase TEST3a = { - T3_INPUT, NULL, "GET", "ftp", "ftp.curl.se", "/xxx?a=2", 2, 0 + T3_INPUT, NULL, NULL, "GET", "ftp", "ftp.curl.se", "/xxx?a=2", 2, 0 }; static const char *T4_INPUT[] = { @@ -155,7 +156,7 @@ static CURLcode test_unit2603(const char *arg) NULL, }; static const struct tcase TEST4a = { - T4_INPUT, NULL, "CONNECT", NULL, "ftp.curl.se:123", NULL, 3, 2 + T4_INPUT, NULL, NULL, "CONNECT", NULL, "ftp.curl.se:123", NULL, 3, 2 }; static const char *T5_INPUT[] = { @@ -165,7 +166,7 @@ static CURLcode test_unit2603(const char *arg) NULL, }; static const struct tcase TEST5a = { - T5_INPUT, NULL, "OPTIONS", NULL, NULL, "*", 2, 3 + T5_INPUT, NULL, NULL, "OPTIONS", NULL, NULL, "*", 2, 3 }; static const char *T6_INPUT[] = { @@ -173,7 +174,19 @@ static CURLcode test_unit2603(const char *arg) NULL, }; static const struct tcase TEST6a = { - T6_INPUT, NULL, "PUT", NULL, NULL, "/path", 1, 3 + T6_INPUT, NULL, NULL, "PUT", NULL, NULL, "/path", 1, 3 + }; + + /* test a custom method with space, #19543 */ + static const char *T7_INPUT[] = { + "IN SANE /path HTTP/1.1\r\nContent-Length: 0\r\n\r\n", + NULL, + }; + static const struct tcase TEST7a = { + T7_INPUT, NULL, NULL, "IN", NULL, NULL, "SANE /path", 1, 0 + }; + static const struct tcase TEST7b = { + T7_INPUT, NULL, "IN SANE", "IN SANE", NULL, NULL, "/path", 1, 0 }; parse_success(&TEST1a); @@ -183,6 +196,8 @@ static CURLcode test_unit2603(const char *arg) parse_success(&TEST4a); parse_success(&TEST5a); parse_success(&TEST6a); + parse_success(&TEST7a); + parse_success(&TEST7b); #endif UNITTEST_END_SIMPLE -- 2.47.3