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
This commit is contained in:
Stefan Eissing 2025-11-17 09:56:48 +01:00 committed by Daniel Stenberg
parent 2459dc7a22
commit ea105708c9
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
8 changed files with 80 additions and 21 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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;

View file

@ -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) {

View file

@ -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) {

View file

@ -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)

View file

@ -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