http: "absolute-form" target URL parsing

Type: improvement

Change-Id: If39680a148d39add40433547369b2ddad3c2e226
Signed-off-by: Matus Fabian <matfabia@cisco.com>
This commit is contained in:
Matus Fabian
2024-10-29 15:06:49 +01:00
parent dcc6cd4333
commit 769a3b7256
7 changed files with 476 additions and 62 deletions

View File

@ -13,6 +13,7 @@ COPY \
$DIR/af_packet_plugin.so \
$DIR/hs_apps_plugin.so \
$DIR/http_plugin.so \
$DIR/http_unittest_plugin.so \
$DIR/unittest_plugin.so \
$DIR/quic_plugin.so \
$DIR/http_static_plugin.so \

View File

@ -31,7 +31,7 @@ func init() {
HttpStaticMacTimeTest, HttpStaticBuildInUrlGetVersionVerboseTest, HttpVersionNotSupportedTest,
HttpInvalidContentLengthTest, HttpInvalidTargetSyntaxTest, HttpStaticPathTraversalTest, HttpUriDecodeTest,
HttpHeadersTest, HttpStaticFileHandlerTest, HttpStaticFileHandlerDefaultMaxAgeTest, HttpClientTest, HttpClientErrRespTest, HttpClientPostFormTest,
HttpClientPostFileTest, HttpClientPostFilePtrTest, AuthorityFormTargetTest, HttpRequestLineTest,
HttpClientPostFileTest, HttpClientPostFilePtrTest, HttpUnitTest, HttpRequestLineTest,
HttpStaticFileHandlerWrkTest, HttpStaticUrlHandlerWrkTest, HttpConnTimeoutTest)
RegisterNoTopoSoloTests(HttpStaticPromTest, HttpGetTpsTest, HttpGetTpsInterruptModeTest, PromConcurrentConnectionsTest,
PromMemLeakTest, HttpClientPostMemLeakTest, HttpInvalidClientRequestMemLeakTest, HttpPostTpsTest, HttpPostTpsInterruptModeTest,
@ -352,24 +352,11 @@ func HttpClientPostFilePtrTest(s *NoTopoSuite) {
httpClientPostFile(s, true, 131072)
}
func cliTestAuthority(s *NoTopoSuite, authority string) {
o := s.GetContainerByName("vpp").VppInstance.Vppctl("test http authority-form " + authority)
s.AssertNotContains(o, "error")
s.AssertContains(o, authority)
}
func cliTestAuthorityError(s *NoTopoSuite, authority string) {
o := s.GetContainerByName("vpp").VppInstance.Vppctl("test http authority-form " + authority)
s.AssertContains(o, "error")
}
func AuthorityFormTargetTest(s *NoTopoSuite) {
cliTestAuthority(s, "10.10.2.45:20")
cliTestAuthority(s, "[dead:beef::1234]:443")
cliTestAuthorityError(s, "example.com:80")
cliTestAuthorityError(s, "10.10.2.45")
cliTestAuthorityError(s, "1000.10.2.45:20")
cliTestAuthorityError(s, "[xyz0::1234]:443")
func HttpUnitTest(s *NoTopoSuite) {
vpp := s.GetContainerByName("vpp").VppInstance
o := vpp.Vppctl("test http all")
s.Log(o)
s.AssertNotContains(o, "FAIL")
}
func HttpStaticPromTest(s *NoTopoSuite) {

View File

@ -60,6 +60,7 @@ plugins {
plugin af_packet_plugin.so { enable }
plugin hs_apps_plugin.so { enable }
plugin http_plugin.so { enable }
plugin http_unittest_plugin.so { enable }
plugin http_static_plugin.so { enable }
plugin prom_plugin.so { enable }
plugin tlsopenssl_plugin.so { enable }

View File

@ -16,5 +16,9 @@ add_vpp_plugin(http
http.c
http_buffer.c
http_timer.c
http_test.c
)
add_vpp_plugin(http_unittest
SOURCES
test/http_test.c
)

View File

@ -447,9 +447,10 @@ typedef struct http_main_
} http_main_t;
always_inline int
_validate_target_syntax (u8 *target, int is_query, int *is_encoded)
_validate_target_syntax (u8 *target, u32 len, int is_query, int *is_encoded)
{
int i, encoded = 0;
int encoded = 0;
u32 i;
static uword valid_chars[4] = {
/* !$&'()*+,-./0123456789:;= */
@ -460,7 +461,7 @@ _validate_target_syntax (u8 *target, int is_query, int *is_encoded)
0x0000000000000000,
};
for (i = 0; i < vec_len (target); i++)
for (i = 0; i < len; i++)
{
if (clib_bitmap_get_no_check (valid_chars, target[i]))
continue;
@ -471,7 +472,7 @@ _validate_target_syntax (u8 *target, int is_query, int *is_encoded)
/* pct-encoded = "%" HEXDIG HEXDIG */
if (target[i] == '%')
{
if ((i + 2) > vec_len (target))
if ((i + 2) >= len)
return -1;
if (!isxdigit (target[i + 1]) || !isxdigit (target[i + 2]))
return -1;
@ -490,7 +491,7 @@ _validate_target_syntax (u8 *target, int is_query, int *is_encoded)
/**
* An "absolute-path" rule validation (RFC9110 section 4.1).
*
* @param path Target path to validate.
* @param path Vector of target path to validate.
* @param is_encoded Return flag that indicates if percent-encoded (optional).
*
* @return @c 0 on success.
@ -498,13 +499,13 @@ _validate_target_syntax (u8 *target, int is_query, int *is_encoded)
always_inline int
http_validate_abs_path_syntax (u8 *path, int *is_encoded)
{
return _validate_target_syntax (path, 0, is_encoded);
return _validate_target_syntax (path, vec_len (path), 0, is_encoded);
}
/**
* A "query" rule validation (RFC3986 section 2.1).
*
* @param query Target query to validate.
* @param query Vector of target query to validate.
* @param is_encoded Return flag that indicates if percent-encoded (optional).
*
* @return @c 0 on success.
@ -512,7 +513,7 @@ http_validate_abs_path_syntax (u8 *path, int *is_encoded)
always_inline int
http_validate_query_syntax (u8 *query, int *is_encoded)
{
return _validate_target_syntax (query, 1, is_encoded);
return _validate_target_syntax (query, vec_len (query), 1, is_encoded);
}
#define htoi(x) (isdigit (x) ? (x - '0') : (tolower (x) - 'a' + 10))
@ -977,6 +978,168 @@ http_serialize_authority_form_target (http_uri_t *authority)
return s;
}
typedef enum http_url_scheme_
{
HTTP_URL_SCHEME_HTTP,
HTTP_URL_SCHEME_HTTPS,
} http_url_scheme_t;
typedef struct
{
http_url_scheme_t scheme;
u16 port;
u32 host_offset;
u32 host_len;
u32 path_offset;
u32 path_len;
u8 host_is_ip6;
} http_url_t;
/**
* An "absolute-form" URL parsing.
*
* @param url Vector of target URL to validate.
* @param parsed Parsed URL metadata in case of success.
*
* @return @c 0 on success.
*/
always_inline int
http_parse_absolute_form (u8 *url, http_url_t *parsed)
{
u8 *token_start, *token_end, *end;
int is_encoded = 0;
static uword valid_chars[4] = {
/* -.0123456789 */
0x03ff600000000000,
/* ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz */
0x07fffffe07fffffe,
0x0000000000000000,
0x0000000000000000,
};
if (vec_len (url) < 9)
{
clib_warning ("uri too short");
return -1;
}
clib_memset (parsed, 0, sizeof (*parsed));
end = url + vec_len (url);
/* parse scheme */
if (!memcmp (url, "http:// ", 7))
{
parsed->scheme = HTTP_URL_SCHEME_HTTP;
parsed->port = clib_host_to_net_u16 (80);
parsed->host_offset = 7;
}
else if (!memcmp (url, "https:// ", 8))
{
parsed->scheme = HTTP_URL_SCHEME_HTTPS;
parsed->port = clib_host_to_net_u16 (443);
parsed->host_offset = 8;
}
else
{
clib_warning ("invalid scheme");
return -1;
}
token_start = url + parsed->host_offset;
/* parse host */
if (*token_start == '[')
/* IPv6 address */
{
parsed->host_is_ip6 = 1;
parsed->host_offset++;
token_end = ++token_start;
while (1)
{
if (token_end == end)
{
clib_warning ("invalid host, IPv6 addr not terminated with ']'");
return -1;
}
else if (*token_end == ']')
{
parsed->host_len = token_end - token_start;
token_start = token_end + 1;
break;
}
else if (*token_end != ':' && *token_end != '.' &&
!isxdigit (*token_end))
{
clib_warning ("invalid character '%u'", *token_end);
return -1;
}
token_end++;
}
}
else
{
token_end = token_start;
while (token_end != end && *token_end != ':' && *token_end != '/')
{
if (!clib_bitmap_get_no_check (valid_chars, *token_end))
{
clib_warning ("invalid character '%u'", *token_end);
return -1;
}
token_end++;
}
parsed->host_len = token_end - token_start;
token_start = token_end;
}
if (!parsed->host_len)
{
clib_warning ("zero length host");
return -1;
}
/* parse port, if any */
if (token_start != end && *token_start == ':')
{
u32 port = 0;
token_end = ++token_start;
while (token_end != end && *token_end != '/')
{
if (isdigit (*token_end))
{
port = port * 10 + *token_end - '0';
if (port > 65535)
{
clib_warning ("invalid port number");
return -1;
}
}
else
{
clib_warning ("expected digit '%u'", *token_end);
return -1;
}
token_end++;
}
parsed->port = clib_host_to_net_u16 ((u16) port);
token_start = token_end;
}
if (token_start == end)
return 0;
token_start++; /* drop leading slash */
parsed->path_offset = token_start - url;
parsed->path_len = end - token_start;
if (parsed->path_len)
return _validate_target_syntax (token_start, parsed->path_len, 0,
&is_encoded);
return 0;
}
#endif /* SRC_PLUGINS_HTTP_HTTP_H_ */
/*

View File

@ -1,34 +0,0 @@
/* SPDX-License-Identifier: Apache-2.0
* Copyright(c) 2024 Cisco Systems, Inc.
*/
#include <http/http.h>
static clib_error_t *
test_http_authority_command_fn (vlib_main_t *vm, unformat_input_t *input,
vlib_cli_command_t *cmd)
{
u8 *target = 0;
http_uri_t authority;
int rv;
if (!unformat (input, "%v", &target))
return clib_error_return (0, "error: no input provided");
rv = http_parse_authority_form_target (target, &authority);
vec_free (target);
if (rv)
return clib_error_return (0, "error: parsing failed");
target = http_serialize_authority_form_target (&authority);
vlib_cli_output (vm, "%v", target);
vec_free (target);
return 0;
}
VLIB_CLI_COMMAND (test_http_authority_command) = {
.path = "test http authority-form",
.short_help = "test dns authority-form",
.function = test_http_authority_command_fn,
};

View File

@ -0,0 +1,292 @@
/* SPDX-License-Identifier: Apache-2.0
* Copyright(c) 2024 Cisco Systems, Inc.
*/
#include <vnet/plugin/plugin.h>
#include <vpp/app/version.h>
#include <http/http.h>
#define HTTP_TEST_I(_cond, _comment, _args...) \
({ \
int _evald = (_cond); \
if (!(_evald)) \
{ \
vlib_cli_output (vm, "FAIL:%d: " _comment "\n", __LINE__, ##_args); \
} \
else \
{ \
vlib_cli_output (vm, "PASS:%d: " _comment "\n", __LINE__, ##_args); \
} \
_evald; \
})
#define HTTP_TEST(_cond, _comment, _args...) \
{ \
if (!HTTP_TEST_I (_cond, _comment, ##_args)) \
{ \
return 1; \
} \
}
static int
http_test_authority_form (vlib_main_t *vm)
{
u8 *target = 0, *formated_target = 0;
http_uri_t authority;
int rv;
target = format (0, "10.10.2.45:20");
rv = http_parse_authority_form_target (target, &authority);
HTTP_TEST ((rv == 0), "'%v' should be valid", target);
formated_target = http_serialize_authority_form_target (&authority);
rv = vec_cmp (target, formated_target);
HTTP_TEST ((rv == 0), "'%v' should match '%v'", target, formated_target);
vec_free (target);
vec_free (formated_target);
target = format (0, "[dead:beef::1234]:443");
rv = http_parse_authority_form_target (target, &authority);
HTTP_TEST ((rv == 0), "'%v' should be valid", target);
formated_target = http_serialize_authority_form_target (&authority);
rv = vec_cmp (target, formated_target);
HTTP_TEST ((rv == 0), "'%v' should match '%v'", target, formated_target);
vec_free (target);
vec_free (formated_target);
target = format (0, "example.com:80");
rv = http_parse_authority_form_target (target, &authority);
HTTP_TEST ((rv != 0), "'%v' reg-name not supported", target);
vec_free (target);
target = format (0, "10.10.2.45");
rv = http_parse_authority_form_target (target, &authority);
HTTP_TEST ((rv != 0), "'%v' should be invalid", target);
vec_free (target);
target = format (0, "1000.10.2.45:20");
rv = http_parse_authority_form_target (target, &authority);
HTTP_TEST ((rv != 0), "'%v' should be invalid", target);
vec_free (target);
target = format (0, "[xyz0::1234]:443");
rv = http_parse_authority_form_target (target, &authority);
HTTP_TEST ((rv != 0), "'%v' should be invalid", target);
vec_free (target);
return 0;
}
static int
http_test_absolute_form (vlib_main_t *vm)
{
u8 *url = 0;
http_url_t parsed_url;
int rv;
url = format (0, "https://example.org/.well-known/masque/udp/1.2.3.4/123/");
clib_warning (
"strlen %u vec_len %u",
strlen ("https://example.org/.well-known/masque/udp/1.2.3.4/123/"),
vec_len (url));
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv == 0), "'%v' should be valid", url);
HTTP_TEST ((parsed_url.scheme == HTTP_URL_SCHEME_HTTPS),
"scheme should be https");
HTTP_TEST ((parsed_url.host_is_ip6 == 0), "host_is_ip6=%u should be 0",
parsed_url.host_is_ip6);
HTTP_TEST ((parsed_url.host_offset == strlen ("https://")),
"host_offset=%u should be %u", parsed_url.host_offset,
strlen ("https://"));
HTTP_TEST ((parsed_url.host_len == strlen ("example.org")),
"host_len=%u should be %u", parsed_url.host_len,
strlen ("example.org"));
HTTP_TEST ((clib_net_to_host_u16 (parsed_url.port) == 443),
"port=%u should be 443", clib_net_to_host_u16 (parsed_url.port));
HTTP_TEST ((parsed_url.path_offset == strlen ("https://example.org/")),
"path_offset=%u should be %u", parsed_url.path_offset,
strlen ("https://example.org/"));
HTTP_TEST (
(parsed_url.path_len == strlen (".well-known/masque/udp/1.2.3.4/123/")),
"path_len=%u should be %u", parsed_url.path_len,
strlen (".well-known/masque/udp/1.2.3.4/123/"));
vec_free (url);
url = format (0, "http://vpp-example.org");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv == 0), "'%v' should be valid", url);
HTTP_TEST ((parsed_url.scheme == HTTP_URL_SCHEME_HTTP),
"scheme should be http");
HTTP_TEST ((parsed_url.host_is_ip6 == 0), "host_is_ip6=%u should be 0",
parsed_url.host_is_ip6);
HTTP_TEST ((parsed_url.host_offset == strlen ("http://")),
"host_offset=%u should be %u", parsed_url.host_offset,
strlen ("http://"));
HTTP_TEST ((parsed_url.host_len == strlen ("vpp-example.org")),
"host_len=%u should be %u", parsed_url.host_len,
strlen ("vpp-example.org"));
HTTP_TEST ((clib_net_to_host_u16 (parsed_url.port) == 80),
"port=%u should be 80", clib_net_to_host_u16 (parsed_url.port));
HTTP_TEST ((parsed_url.path_len == 0), "path_len=%u should be 0",
parsed_url.path_len);
vec_free (url);
url = format (0, "http://1.2.3.4:8080/abcd");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv == 0), "'%v' should be valid", url);
HTTP_TEST ((parsed_url.scheme == HTTP_URL_SCHEME_HTTP),
"scheme should be http");
HTTP_TEST ((parsed_url.host_is_ip6 == 0), "host_is_ip6=%u should be 0",
parsed_url.host_is_ip6);
HTTP_TEST ((parsed_url.host_offset == strlen ("http://")),
"host_offset=%u should be %u", parsed_url.host_offset,
strlen ("http://"));
HTTP_TEST ((parsed_url.host_len == strlen ("1.2.3.4")),
"host_len=%u should be %u", parsed_url.host_len,
strlen ("1.2.3.4"));
HTTP_TEST ((clib_net_to_host_u16 (parsed_url.port) == 8080),
"port=%u should be 8080", clib_net_to_host_u16 (parsed_url.port));
HTTP_TEST ((parsed_url.path_offset == strlen ("http://1.2.3.4:8080/")),
"path_offset=%u should be %u", parsed_url.path_offset,
strlen ("http://1.2.3.4:8080/"));
HTTP_TEST ((parsed_url.path_len == strlen ("abcd")),
"path_len=%u should be %u", parsed_url.path_len, strlen ("abcd"));
vec_free (url);
url = format (0, "https://[dead:beef::1234]/abcd");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv == 0), "'%v' should be valid", url);
HTTP_TEST ((parsed_url.scheme == HTTP_URL_SCHEME_HTTPS),
"scheme should be https");
HTTP_TEST ((parsed_url.host_is_ip6 == 1), "host_is_ip6=%u should be 1",
parsed_url.host_is_ip6);
HTTP_TEST ((parsed_url.host_offset == strlen ("https://[")),
"host_offset=%u should be %u", parsed_url.host_offset,
strlen ("https://["));
HTTP_TEST ((parsed_url.host_len == strlen ("dead:beef::1234")),
"host_len=%u should be %u", parsed_url.host_len,
strlen ("dead:beef::1234"));
HTTP_TEST ((clib_net_to_host_u16 (parsed_url.port) == 443),
"port=%u should be 443", clib_net_to_host_u16 (parsed_url.port));
HTTP_TEST ((parsed_url.path_offset == strlen ("https://[dead:beef::1234]/")),
"path_offset=%u should be %u", parsed_url.path_offset,
strlen ("https://[dead:beef::1234]/"));
HTTP_TEST ((parsed_url.path_len == strlen ("abcd")),
"path_len=%u should be %u", parsed_url.path_len, strlen ("abcd"));
vec_free (url);
url = format (0, "http://[::ffff:192.0.2.128]:8080/");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv == 0), "'%v' should be valid", url);
HTTP_TEST ((parsed_url.scheme == HTTP_URL_SCHEME_HTTP),
"scheme should be http");
HTTP_TEST ((parsed_url.host_is_ip6 == 1), "host_is_ip6=%u should be 1",
parsed_url.host_is_ip6);
HTTP_TEST ((parsed_url.host_offset == strlen ("http://[")),
"host_offset=%u should be %u", parsed_url.host_offset,
strlen ("http://["));
HTTP_TEST ((parsed_url.host_len == strlen ("::ffff:192.0.2.128")),
"host_len=%u should be %u", parsed_url.host_len,
strlen ("::ffff:192.0.2.128"));
HTTP_TEST ((clib_net_to_host_u16 (parsed_url.port) == 8080),
"port=%u should be 8080", clib_net_to_host_u16 (parsed_url.port));
HTTP_TEST ((parsed_url.path_len == 0), "path_len=%u should be 0",
parsed_url.path_len);
vec_free (url);
url = format (0, "http://[dead:beef::1234/abc");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
url = format (0, "http://[dead|beef::1234]/abc");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
url = format (0, "http:example.org:8080/abcd");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
url = format (0, "htt://example.org:8080/abcd");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
url = format (0, "http://");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
url = format (0, "http:///abcd");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
url = format (0, "http://example.org:808080/abcd");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
url = format (0, "http://example.org/a%%3Xbcd");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
url = format (0, "http://example.org/a%%3");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
url = format (0, "http://example.org/a[b]cd");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
url = format (0, "http://exa[m]ple.org/abcd");
rv = http_parse_absolute_form (url, &parsed_url);
HTTP_TEST ((rv != 0), "'%v' should be invalid", url);
vec_free (url);
return 0;
}
static clib_error_t *
test_http_command_fn (vlib_main_t *vm, unformat_input_t *input,
vlib_cli_command_t *cmd)
{
int res = 0;
while (unformat_check_input (input) != UNFORMAT_END_OF_INPUT)
{
if (unformat (input, "authority-form"))
res = http_test_authority_form (vm);
else if (unformat (input, "absolute-form"))
res = http_test_absolute_form (vm);
else if (unformat (input, "all"))
{
if ((res = http_test_authority_form (vm)))
goto done;
if ((res = http_test_absolute_form (vm)))
goto done;
}
else
break;
}
done:
if (res)
return clib_error_return (0, "HTTP unit test failed");
return 0;
}
VLIB_CLI_COMMAND (test_http_command) = {
.path = "test http",
.short_help = "http unit tests",
.function = test_http_command_fn,
};
VLIB_PLUGIN_REGISTER () = {
.version = VPP_BUILD_VER,
.description = "HTTP - Unit Test",
.default_disabled = 1,
};