blob: 2fc62bcdfda2badb07451990910a47fe38d0762d [file]
/** @file
Catch-based unit tests for URL
@section license License
Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.
See the NOTICE file distributed with this work for additional information regarding copyright
ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License. You may obtain a
copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing permissions and limitations under
the License.
*/
#include <cctype>
#include <cstdio>
#include <cstring>
#include <memory>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/generators/catch_generators_range.hpp>
#include "proxy/hdrs/URL.h"
#include "tscore/CryptoHash.h"
TEST_CASE("ValidateURL", "[proxy][validurl]")
{
struct Test {
const char *const text;
bool valid;
};
static const std::vector<Test> http_validate_hdr_field_test_case = {
{"yahoo", true },
{"yahoo.com", true },
{"yahoo.wow.com", true },
{"yahoo.wow.much.amaze.com", true },
{"209.131.52.50", true },
{"192.168.0.1", true },
{"localhost", true },
{"3ffe:1900:4545:3:200:f8ff:fe21:67cf", true },
{"fe80:0:0:0:200:f8ff:fe21:67cf", true },
{"fe80::200:f8ff:fe21:67cf", true },
{"<svg onload=alert(1)>", false}, // Sample host header XSS attack
{"jlads;f8-9349*(D&F*D(234jD*(FSD*(VKLJ#(*$@()#$)))))", false},
{"\"\t\n", false},
{"!@#$%^ &*(*&^%$#@#$%^&*(*&^%$#))", false},
{":):(:O!!!!!!", false}
};
auto i = GENERATE(from_range(http_validate_hdr_field_test_case));
CAPTURE(i.text, i.valid);
CHECK(validate_host_name({i.text}) == i.valid);
}
TEST_CASE("Validate Scheme", "[proxy][validscheme]")
{
struct Test {
std::string_view text;
bool valid;
};
static const std::vector<Test> scheme_test_cases = {
{"http", true },
{"https", true },
{"example", true },
{"example.", true },
{"example++", true },
{"example--.", true },
{"http+unix", true },
{"https+unix", true },
{"++example", false},
{"--example", false},
{".example", false},
{"example://", false}
};
auto i = GENERATE(from_range(scheme_test_cases));
CAPTURE(i.text, i.valid);
CHECK(validate_scheme(i.text) == i.valid);
}
namespace UrlImpl
{
bool url_is_strictly_compliant(const char *start, const char *end);
bool url_is_mostly_compliant(const char *start, const char *end);
} // namespace UrlImpl
using namespace UrlImpl;
TEST_CASE("ParseRulesStrictURI", "[proxy][parseuri]")
{
struct Test {
const char *const uri;
bool valid;
};
static const std::vector<Test> http_strict_uri_parsing_test_case = {
{"//index.html", true },
{"/home", true },
{"/path/data?key=value#id", true },
{"/ABCDEFGHIJKLMNOPQRSTUVWXYZ", true },
{"/abcdefghijklmnopqrstuvwxyz", true },
{"/abcde fghijklmnopqrstuvwxyz", false},
{"/abcde\tfghijklmnopqrstuvwxyz", false},
{"/abcdefghijklmnopqrstuvwxyz", false},
{"/0123456789", true },
{":/?#[]@", true },
{"!$&'()*+,;=", true },
{"-._~", true },
{"%", true },
{"\n", false},
{"\"", false},
{"<", false},
{">", false},
{"\\", false},
{"^", false},
{"`", false},
{"{", false},
{"|", false},
{"}", false},
{"é", false}
};
auto i = GENERATE(from_range(http_strict_uri_parsing_test_case));
CAPTURE(i.uri, i.valid);
CHECK(url_is_strictly_compliant(i.uri, i.uri + strlen(i.uri)) == i.valid);
}
TEST_CASE("ParseRulesMostlyStrictURI", "[proxy][parseuri]")
{
struct Test {
const char *const uri;
bool valid;
};
static const std::vector<Test> http_mostly_strict_uri_parsing_test_case = {
{"//index.html", true },
{"/home", true },
{"/path/data?key=value#id", true },
{"/ABCDEFGHIJKLMNOPQRSTUVWXYZ", true },
{"/abcdefghijklmnopqrstuvwxyz", true },
{"/abcde fghijklmnopqrstuvwxyz", false},
{"/abcde\tfghijklmnopqrstuvwxyz", false},
{"/abcdefghijklmnopqrstuvwxyz", false},
{"/0123456789", true },
{":/?#[]@", true },
{"!$&'()*+,;=", true },
{"-._~", true },
{"%", true },
{"\n", false},
{"\"", true },
{"<", true },
{">", true },
{"\\", true },
{"^", true },
{"`", true },
{"{", true },
{"|", true },
{"}", true },
{"é", false}
}; // Non-printable ascii
auto i = GENERATE(from_range(http_mostly_strict_uri_parsing_test_case));
CAPTURE(i.uri, i.valid);
CHECK(url_is_mostly_compliant(i.uri, i.uri + strlen(i.uri)) == i.valid);
}
namespace
{
// The isspace()/isprint() pair that url_is_mostly_compliant replaced, kept as
// the reference so this compares old behavior against new instead of restating
// the new range test. Bytes are passed as unsigned char; the original passed a
// plain char, which is undefined for the negative values every byte >= 0x80
// produces. In the C locale, which ATS never leaves (nothing in the tree calls
// setlocale), this accepts exactly 0x21..0x7E.
bool
url_mostly_compliant_reference(const char *start, const char *end)
{
for (const char *p = start; p < end; ++p) {
unsigned char const c = static_cast<unsigned char>(*p);
if (isspace(c) || !isprint(c)) {
return false;
}
}
return true;
}
} // namespace
TEST_CASE("MostlyCompliantVsScalar", "[proxy][parseuri]")
{
// The auto-vectorized url_is_mostly_compliant must agree with the libc pair
// it replaced for every input. Exhaustively inject each of the 256 byte values
// at each position across lengths 1..40, which span a sub-vector target, a
// whole vector, and the scalar remainder for both 128- and 256-bit builds.
char buf[64];
int mismatches = 0, first_len = 0, first_pos = 0, first_byte = 0;
for (int len = 1; len <= 40; ++len) {
for (int pos = 0; pos < len; ++pos) {
for (int b = 0; b < 256; ++b) {
memset(buf, 'a', len); // 'a' (0x61) is compliant
buf[pos] = static_cast<char>(b);
bool const got = url_is_mostly_compliant(buf, buf + len);
bool const want = url_mostly_compliant_reference(buf, buf + len);
if (got != want) {
if (mismatches == 0) {
first_len = len;
first_pos = pos;
first_byte = b;
}
++mismatches;
}
}
}
}
CAPTURE(mismatches, first_len, first_pos, first_byte);
CHECK(mismatches == 0);
// Boundary extremes at every length: all-0x7E accepts, all-0x7F rejects.
for (int len = 0; len <= 40; ++len) {
memset(buf, 0x7E, len);
CHECK(url_is_mostly_compliant(buf, buf + len) == true);
if (len > 0) {
memset(buf, 0x7F, len);
CHECK(url_is_mostly_compliant(buf, buf + len) == false);
}
}
}
struct url_parse_test_case {
const std::string input_uri;
const std::string expected_printed_url;
const bool verify_host_characters;
const std::string expected_printed_url_regex;
const bool is_valid;
const bool is_valid_regex;
};
constexpr bool IS_VALID = true;
constexpr bool VERIFY_HOST_CHARACTERS = true;
// clang-format off
std::vector<url_parse_test_case> url_parse_test_cases = {
{
"///dir////index.html",
"/dir////index.html",
VERIFY_HOST_CHARACTERS,
"/dir////index.html",
IS_VALID,
IS_VALID
},
{
"/index.html",
"/index.html",
VERIFY_HOST_CHARACTERS,
"/index.html",
IS_VALID,
IS_VALID
},
{
"//index.html",
"/index.html",
VERIFY_HOST_CHARACTERS,
"/index.html",
IS_VALID,
IS_VALID
},
{
// The following scheme-only URI is technically valid per the spec, but we
// have historically returned this as invalid and I'm not comfortable
// changing it in case something depends upon this behavior. Besides, a
// scheme-only URI is probably not helpful to us nor something likely
// Traffic Server will see.
"http://",
"",
VERIFY_HOST_CHARACTERS,
"",
!IS_VALID,
!IS_VALID
},
{
"https:///",
"https:///",
VERIFY_HOST_CHARACTERS,
"https:///",
IS_VALID,
IS_VALID
},
{
// RFC 3986 section-3: When authority is not present, the path cannot begin
// with two slash characters ("//"). We have historically allowed this,
// however, and will continue to do so.
"https:////",
"https:///",
VERIFY_HOST_CHARACTERS,
"https:///",
IS_VALID,
IS_VALID
},
{
// By convention, our url_print() function adds a path of '/' at the end of
// URLs that have no path, query, or fragment after the authority.
"mailto:Test.User@example.com",
"mailto:Test.User@example.com/",
VERIFY_HOST_CHARACTERS,
"mailto:Test.User@example.com/",
IS_VALID,
IS_VALID
},
{
"mailto:Test.User@example.com:25",
"mailto:Test.User@example.com:25/",
VERIFY_HOST_CHARACTERS,
"mailto:Test.User@example.com:25/",
IS_VALID,
IS_VALID
},
{
"https://www.example.com",
"https://www.example.com/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/",
IS_VALID,
IS_VALID
},
{
"https://www.example.com/",
"https://www.example.com/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/",
IS_VALID,
IS_VALID
},
{
"https://www.example.com//",
"https://www.example.com/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/",
IS_VALID,
IS_VALID
},
{
"https://127.0.0.1",
"https://127.0.0.1/",
VERIFY_HOST_CHARACTERS,
"https://127.0.0.1/",
IS_VALID,
IS_VALID
},
{
"https://[::1]",
"https://[::1]/",
VERIFY_HOST_CHARACTERS,
"https://[::1]/",
IS_VALID,
IS_VALID
},
{
"https://127.0.0.1/",
"https://127.0.0.1/",
VERIFY_HOST_CHARACTERS,
"https://127.0.0.1/",
IS_VALID,
IS_VALID
},
{
"https://www.example.com:8888",
"https://www.example.com:8888/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com:8888/",
IS_VALID,
IS_VALID
},
{
"https://www.example.com:8888/",
"https://www.example.com:8888/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com:8888/",
IS_VALID,
IS_VALID
},
{
// Maximum value that fits in the 16-bit port field; preserved verbatim.
"https://www.example.com:65535/",
"https://www.example.com:65535/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com:65535/",
IS_VALID,
IS_VALID
},
{
// One past the 16-bit boundary: the port is rejected and the URL is
// emitted with no explicit port so default-port logic applies.
"https://www.example.com:65536/",
"https://www.example.com/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/",
IS_VALID,
IS_VALID
},
{
// Five-digit value above the 16-bit range.
"https://www.example.com:99999/",
"https://www.example.com/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/",
IS_VALID,
IS_VALID
},
{
// Six-digit value whose low 16 bits coincide with a well-known port (80);
// must not be silently retained as if the user had asked for that port.
"https://www.example.com:131152/",
"https://www.example.com/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/",
IS_VALID,
IS_VALID
},
{
"https://www.example.com/a/path",
"https://www.example.com/a/path",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path",
IS_VALID,
IS_VALID
},
{
"https://www.example.com//a/path",
"https://www.example.com/a/path",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path",
IS_VALID,
IS_VALID
},
// Technically a trailing '?' with an empty query string is valid, but we
// drop the '?'. The parse_regex, however, makes no distinction between
// query, fragment, and path components so it does not cut it out.
{
"https://www.example.com/a/path?",
"https://www.example.com/a/path",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path?",
IS_VALID,
IS_VALID},
{
"https://www.example.com/a/path?name=value",
"https://www.example.com/a/path?name=value",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path?name=value",
IS_VALID,
IS_VALID
},
{
"https://www.example.com/a/path?name=/a/path/value",
"https://www.example.com/a/path?name=/a/path/value",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path?name=/a/path/value",
IS_VALID,
IS_VALID
},
{
"https://www.example.com/a/path?name=/a/path/value;some=other_value",
"https://www.example.com/a/path?name=/a/path/value;some=other_value",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path?name=/a/path/value;some=other_value",
IS_VALID,
IS_VALID
},
{
"https://www.example.com/a/path?name=/a/path/value;some=other_value/",
"https://www.example.com/a/path?name=/a/path/value;some=other_value/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path?name=/a/path/value;some=other_value/",
IS_VALID,
IS_VALID
},
// Again, URL::parse drops a final '?'.
{
"https://www.example.com?",
"https://www.example.com",
VERIFY_HOST_CHARACTERS,
"https://www.example.com?/",
IS_VALID,
IS_VALID
},
{
"https://www.example.com?name=value",
"https://www.example.com?name=value",
VERIFY_HOST_CHARACTERS,
"https://www.example.com?name=value/",
IS_VALID,
IS_VALID
},
{
"https://www.example.com?name=value/",
"https://www.example.com?name=value/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com?name=value/",
IS_VALID,
IS_VALID
},
// URL::parse also drops the final '#'.
{
"https://www.example.com#",
"https://www.example.com",
VERIFY_HOST_CHARACTERS,
"https://www.example.com#/",
IS_VALID,
IS_VALID
},
{
"https://www.example.com#some=value",
"https://www.example.com#some=value",
VERIFY_HOST_CHARACTERS,
"https://www.example.com#some=value/",
IS_VALID,
IS_VALID},
{
"https://www.example.com/a/path#",
"https://www.example.com/a/path",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path#",
IS_VALID,
IS_VALID
},
{
"https://www.example.com/a/path#some=value",
"https://www.example.com/a/path#some=value",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path#some=value",
IS_VALID,
IS_VALID
},
{
// Note that this final '?' is not for a query parameter but is a part of
// the fragment.
"https://www.example.com/a/path#some=value?",
"https://www.example.com/a/path#some=value?",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path#some=value?",
IS_VALID,
IS_VALID
},
{
"https://www.example.com/a/path#some=value?with_question",
"https://www.example.com/a/path#some=value?with_question",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path#some=value?with_question",
IS_VALID,
IS_VALID
},
{
"https://www.example.com/a/path?name=value?_with_question#some=value?with_question/",
"https://www.example.com/a/path?name=value?_with_question#some=value?with_question/",
VERIFY_HOST_CHARACTERS,
"https://www.example.com/a/path?name=value?_with_question#some=value?with_question/",
IS_VALID,
IS_VALID
},
// The following are some examples of strings we expect from regex_map in
// remap.config. The "From" portion, which are regular expressions, are
// often not parsible by URL::parse but are by URL::parse_regex, which is the
// purpose of its existence.
{
R"(http://(.*)?reactivate\.mail\.yahoo\.com/)",
"",
VERIFY_HOST_CHARACTERS,
R"(http://(.*)?reactivate\.mail\.yahoo\.com/)",
!IS_VALID,
IS_VALID
},
{
// The following is an example of a "To" URL in a regex_map line. We'll
// first verify that the '$' is flagged as invalid for a host in this case.
"http://$1reactivate.real.mail.yahoo.com/",
"http://$1reactivate.real.mail.yahoo.com/",
VERIFY_HOST_CHARACTERS,
"http://$1reactivate.real.mail.yahoo.com/",
!IS_VALID,
IS_VALID
},
{
// Same as above, but this time we pass in !VERIFY_HOST_CHARACTERS. This is
// how RemapConfig will call this parse() function.
"http://$1reactivate.real.mail.yahoo.com/",
"http://$1reactivate.real.mail.yahoo.com/",
!VERIFY_HOST_CHARACTERS,
"http://$1reactivate.real.mail.yahoo.com/",
IS_VALID,
IS_VALID
}
};
// clang-format on
constexpr bool URL_PARSE = true;
constexpr bool URL_PARSE_REGEX = false;
/** Test the specified url.parse function.
*
* URL::parse and URL::parse_regex should behave the same. This function
* performs the same behavior for each.
*
* @param[in] test_case The test case specification to run.
*
* @param[in] parse_function Whether to run parse() or
* parse_regex().
*/
void
test_parse(url_parse_test_case const &test_case, bool parse_function)
{
URL url;
HdrHeap *heap = new_HdrHeap();
url.create(heap);
ParseResult result = ParseResult::OK;
if (parse_function == URL_PARSE) {
if (test_case.verify_host_characters) {
result = url.parse(test_case.input_uri);
} else {
result = url.parse_no_host_check(test_case.input_uri);
}
} else if (parse_function == URL_PARSE_REGEX) {
result = url.parse_regex(test_case.input_uri);
}
bool expected_is_valid = test_case.is_valid;
if (parse_function == URL_PARSE_REGEX) {
expected_is_valid = test_case.is_valid_regex;
}
if (expected_is_valid && result != ParseResult::DONE) {
std::printf("Parse URI: \"%s\", expected it to be valid but it was parsed invalid (%d)\n", test_case.input_uri.c_str(),
static_cast<int>(result));
CHECK(false);
} else if (!expected_is_valid && result != ParseResult::ERROR) {
std::printf("Parse URI: \"%s\", expected it to be invalid but it was parsed valid (%d)\n", test_case.input_uri.c_str(),
static_cast<int>(result));
CHECK(false);
}
if (result == ParseResult::DONE) {
char buf[1024];
int index = 0;
int offset = 0;
url.print(buf, sizeof(buf), &index, &offset);
std::string printed_url{buf, static_cast<size_t>(index)};
if (parse_function == URL_PARSE) {
CHECK(test_case.expected_printed_url == printed_url);
CHECK(test_case.expected_printed_url.size() == printed_url.size());
} else if (parse_function == URL_PARSE_REGEX) {
CHECK(test_case.expected_printed_url_regex == printed_url);
CHECK(test_case.expected_printed_url_regex.size() == printed_url.size());
}
}
heap->destroy();
}
TEST_CASE("UrlParse", "[proxy][parseurl]")
{
auto test_case = GENERATE(from_range(url_parse_test_cases));
CAPTURE(test_case.input_uri, test_case.expected_printed_url, test_case.is_valid);
test_parse(test_case, URL_PARSE);
test_parse(test_case, URL_PARSE_REGEX);
}
TEST_CASE("UrlParsePortStorage", "[proxy][parseurl]")
{
// Validate the underlying URLImpl port storage rather than the printed form,
// so the rejection path cannot leave the parsed text behind even when other
// serialization paths only inspect m_ptr_port.
struct Case {
std::string input_uri;
int expected_port; // numeric m_port value
bool expect_port_text; // true if m_ptr_port should be non-null
};
// clang-format off
static const std::vector<Case> cases = {
// In-range ports keep both the numeric value and the parsed text.
{"https://www.example.com:8080/", 8080, true },
{"https://www.example.com:65535/", 65535, true },
// Out-of-range ports clear both.
{"https://www.example.com:65536/", 0, false},
{"https://www.example.com:99999/", 0, false},
{"https://www.example.com:131152/", 0, false},
};
// clang-format on
auto c = GENERATE(from_range(cases));
CAPTURE(c.input_uri, c.expected_port, c.expect_port_text);
URL url;
HdrHeap *heap = new_HdrHeap();
url.create(heap);
REQUIRE(url.parse(c.input_uri) == ParseResult::DONE);
CHECK(url.m_url_impl->m_port == c.expected_port);
if (c.expect_port_text) {
CHECK(url.m_url_impl->m_ptr_port != nullptr);
CHECK(url.m_url_impl->m_len_port > 0);
} else {
CHECK(url.m_url_impl->m_ptr_port == nullptr);
CHECK(url.m_url_impl->m_len_port == 0);
}
heap->destroy();
}
TEST_CASE("UrlPrintLowerCaseSchemeHostHandlesHighBitBytes", "[proxy][urlprint]")
{
constexpr auto HIGH_ORDER_BIT = static_cast<char>(0x80);
std::string input_uri{"HTTP://High"};
input_uri.push_back(HIGH_ORDER_BIT);
input_uri.append(".Example/path");
std::string expected_url{"http://high"};
expected_url.push_back(HIGH_ORDER_BIT);
expected_url.append(".example/path");
URL url;
HdrHeap *heap = new_HdrHeap();
url.create(heap);
REQUIRE(url.parse_no_host_check(input_uri) == ParseResult::DONE);
int length = 0;
char *printed_url = url.string_get_ref(&length, URLNormalize::LC_SCHEME_HOST);
REQUIRE(printed_url != nullptr);
CHECK(std::string_view{printed_url, static_cast<std::string_view::size_type>(length)} == expected_url);
heap->destroy();
}
struct get_hash_test_case {
const std::string description;
const std::string uri_1;
const std::string uri_2;
const bool ignore_query;
const bool has_equal_hash;
};
constexpr bool HAS_EQUAL_HASH = true;
constexpr bool IGNORE_QUERY = true;
// clang-format off
std::vector<get_hash_test_case> get_hash_test_cases = {
{
"No encoding: equal hashes",
"http://one.example.com/a/path?name=value#some=value?with_question#fragment",
"http://one.example.com/a/path?name=value#some=value?with_question#fragment",
!IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
"Scheme encoded: equal hashes",
"http%3C://one.example.com/a/path?name=value#some=value?with_question#fragment",
"http<://one.example.com/a/path?name=value#some=value?with_question#fragment",
!IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
"Host encoded: equal hashes",
"http://one%2Eexample.com/a/path?name=value#some=value?with_question#fragment",
"http://one.example.com/a/path?name=value#some=value?with_question#fragment",
!IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
"Path encoded: differing hashes",
"http://one.example.com/a%2Fpath?name=value#some=value?with_question#fragment",
"http://one.example.com/a/path?name=value#some=value?with_question#fragment",
!IGNORE_QUERY,
!HAS_EQUAL_HASH,
},
{
"Query = encoded: differing hashes",
"http://one.example.com/a/path?name%3Dvalue#some=value?with_question#fragment",
"http://one.example.com/a/path?name=value#some=value?with_question#fragment",
!IGNORE_QUERY,
!HAS_EQUAL_HASH,
},
{
"Query = encoded but ignore_query: equal hashes",
"http://one.example.com/a/path?name%3Dvalue#some=value?with_question#fragment",
"http://one.example.com/a/path?name=value#some=value?with_question#fragment",
IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
"Query internal encoded: differing hashes",
"http://one.example.com/a/path?name=valu%5D#some=value?with_question#fragment",
"http://one.example.com/a/path?name=valu]#some=value?with_question#fragment",
!IGNORE_QUERY,
!HAS_EQUAL_HASH,
},
{
"Query internal encoded but ignore_query: equal hashes",
"http://one.example.com/a/path?name=valu%5D#some=value?with_question#fragment",
"http://one.example.com/a/path?name=valu]#some=value?with_question#fragment",
IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
"Fragment encoded: fragment is not part of the hash",
"http://one.example.com/a/path?name=value#some=value?with_question#frag%7Dent",
"http://one.example.com/a/path?name=value#some=value?with_question/frag}ent",
!IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
"Username encoded: equal hashes",
"mysql://my%7Eser:mypassword@localhost/mydatabase",
"mysql://my~ser:mypassword@localhost/mydatabase",
!IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
"Password encoded: equal hashes",
"mysql://myuser:mypa%24sword@localhost/mydatabase",
"mysql://myuser:mypa$sword@localhost/mydatabase",
!IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
"Uppercase host: equal hashes",
"http://ONE.EXAMPLE.COM/a/path?name=value",
"http://one.example.com/a/path?name=value",
!IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
"Mixed-case host: equal hashes",
"http://One.Example.Com/a/path?name=value",
"http://one.example.com/a/path?name=value",
!IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
"Uppercase scheme: equal hashes",
"HTTP://one.example.com/a/path?name=value",
"http://one.example.com/a/path?name=value",
!IGNORE_QUERY,
HAS_EQUAL_HASH,
},
{
// Long uppercase host crosses 16- and 32-byte SIMD body boundaries so
// the wider paths (when compiled in) are exercised by this fixture.
"Long uppercase host: equal hashes",
"http://A-VERY-LONG-HOST-NAME-FOR-SIMD.EXAMPLE.COM/a/path",
"http://a-very-long-host-name-for-simd.example.com/a/path",
!IGNORE_QUERY,
HAS_EQUAL_HASH,
},
};
/** Return the hash related to a URI.
*
* @param[in] uri The URI to hash.
* @return The hash of the URI.
*/
CryptoHash
get_hash(const std::string &uri, bool ignore_query)
{
URL url;
HdrHeap *heap = new_HdrHeap();
url.create(heap);
url.parse(uri);
CryptoHash hash;
url.hash_get(&hash, ignore_query);
heap->destroy();
return hash;
}
TEST_CASE("UrlHashGet", "[url][hash_get]")
{
for (auto const &test_case : get_hash_test_cases) {
std::string description = test_case.description + ": " + test_case.uri_1 + " vs " + test_case.uri_2;
SECTION(description) {
CryptoHash hash1 = get_hash(test_case.uri_1, test_case.ignore_query);
CryptoHash hash2 = get_hash(test_case.uri_2, test_case.ignore_query);
if (test_case.has_equal_hash) {
CHECK(hash1 == hash2);
} else {
CHECK(hash1 != hash2);
}
}
}
}
struct get_path_test_case {
const std::string description;
const std::string uri;
const std::string path;
};
// clang-format off
std::vector<get_path_test_case> get_path_test_cases = {
{
"Semicolon in paths 1",
"http://foo.test/abc/xyz;p1=1,p2=2",
"abc/xyz;p1=1,p2=2",
},
{
"Semicolon in paths 2",
"http://foo.test/abc;p1=1,p2=2/xyz",
"abc;p1=1,p2=2/xyz",
},
{
"Semicolon in paths 3",
"http://foo.test/abc/xyz;p1=1,p2=2?q1=1",
"abc/xyz;p1=1,p2=2",
},
{
"Semicolon in paths 4",
"http://foo.test/abc;p1=1,p2=2/xyz?q1=1",
"abc;p1=1,p2=2/xyz",
},
};
/** Return the hash related to a URI.
*
* @param[in] uri The URI to hash.
* @return The hash of the URI.
*/
TEST_CASE("UrlPathGet", "[url][path_get]")
{
for (auto const &test_case : get_path_test_cases) {
std::string description = test_case.description + ": " + test_case.uri + " -> " + test_case.path;
SECTION(description) {
URL url;
HdrHeap *heap = new_HdrHeap();
url.create(heap);
url.parse(test_case.uri);
auto path{url.path_get()};
CHECK(path == test_case.path);
heap->destroy();
}
}
}
// ATS 9.2 hashed "path" ";" "params" as separate cache-key components, always
// emitting the separator. The params component was removed from the parser, so
// ";params" now lives inside the path and the separator is already there. That
// makes the 9.2 key expressible through the current algorithm: it is the
// current key of the same URL with exactly one ";" between path and query.
namespace
{
/// A failing REQUIRE unwinds out of these helpers, so the heap is released by
/// scope exit rather than by a call that the unwind would skip.
struct HdrHeapDeleter {
void
operator()(HdrHeap *heap) const noexcept
{
heap->destroy();
}
};
using HdrHeapPtr = std::unique_ptr<HdrHeap, HdrHeapDeleter>;
CryptoHash
hash92(char const *text)
{
HdrHeapPtr heap{new_HdrHeap()};
URL url;
url.create(heap.get());
REQUIRE(url.parse(text, strlen(text)) == ParseResult::DONE);
CryptoHash hash;
url.hash_get92(&hash);
return hash;
}
CryptoHash
hash_current(char const *text)
{
HdrHeapPtr heap{new_HdrHeap()};
URL url;
url.create(heap.get());
REQUIRE(url.parse(text, strlen(text)) == ParseResult::DONE);
CryptoHash hash;
url.hash_get(&hash);
return hash;
}
bool
has_params(char const *text)
{
HdrHeapPtr heap{new_HdrHeap()};
URL url;
url.create(heap.get());
REQUIRE(url.parse(text, strlen(text)) == ParseResult::DONE);
return url.has_path_params();
}
} // namespace
TEST_CASE("UrlHashGet92 reproduces the 9.2 cache key", "[url][hash_get92]")
{
SECTION("a path without params gains the 9.2 separator")
{
CHECK(hash92("http://foo.test/path") == hash_current("http://foo.test/path;"));
CHECK(hash92("http://foo.test/path?q=1") == hash_current("http://foo.test/path;?q=1"));
// A ';' inside the query is not a params segment: 9.2 stopped the params
// component at '?', so the path still gained a separator of its own.
CHECK(hash92("http://foo.test/a?b;c") == hash_current("http://foo.test/a;?b;c"));
}
SECTION("a path carrying params already spells the 9.2 key")
{
CHECK(hash92("http://foo.test/a;b=1") == hash_current("http://foo.test/a;b=1"));
CHECK(hash92("http://foo.test/a;b;c") == hash_current("http://foo.test/a;b;c"));
CHECK(hash92("http://foo.test/a;b=1?q=1") == hash_current("http://foo.test/a;b=1?q=1"));
}
SECTION("the keys diverge only when the path has no params segment")
{
// This divergence is the whole reason the compatibility lookup exists.
CHECK(hash92("http://foo.test/path") != hash_current("http://foo.test/path"));
CHECK(hash92("http://foo.test/path?q=1") != hash_current("http://foo.test/path?q=1"));
// ... and when it converges there is nothing for a second lookup to find.
CHECK(hash92("http://foo.test/a;b=1") == hash_current("http://foo.test/a;b=1"));
}
SECTION("an escaped %3B is not a params segment")
{
// 9.2 split on the literal ';' only and hashed the path without unescaping,
// so "%3B" stayed in the path and the separator was still appended. The
// params check has to look at the raw path bytes, not the decoded ones.
CHECK(hash92("http://foo.test/a%3Bb=1") == hash_current("http://foo.test/a%3Bb=1;"));
CHECK(hash92("http://foo.test/a%3Bb=1?q=1") == hash_current("http://foo.test/a%3Bb=1;?q=1"));
CHECK(hash92("http://foo.test/a%3bb=1") == hash_current("http://foo.test/a%3bb=1;"));
// The escaped and literal forms were cached separately and must stay apart.
CHECK(hash92("http://foo.test/a%3Bb=1") != hash92("http://foo.test/a;b=1"));
}
SECTION("has_path_params identifies the converging case")
{
CHECK(!has_params("http://foo.test/path"));
CHECK(!has_params("http://foo.test/a?b;c"));
CHECK(!has_params("http://foo.test/a%3Bb=1"));
CHECK(has_params("http://foo.test/a;b=1"));
CHECK(has_params("http://foo.test/a;b=1?q=1"));
}
}
// URL getters must not construct std::string_view from a nullptr pointer
// (which is UB). Parts that are not present in the URL should return an
// empty string_view with data() == nullptr.
TEST_CASE("UrlMissingParts", "[url][missing_parts]")
{
URL url;
HdrHeap *heap = new_HdrHeap();
url.create(heap);
// A freshly created URL with no parse has no components set.
auto scheme{url.scheme_get()};
auto user{url.user_get()};
auto password{url.password_get()};
auto host{url.host_get()};
auto path{url.path_get()};
auto query{url.query_get()};
auto fragment{url.fragment_get()};
CHECK(scheme.empty());
CHECK(scheme.data() == nullptr);
CHECK(user.empty());
CHECK(user.data() == nullptr);
CHECK(password.empty());
CHECK(password.data() == nullptr);
CHECK(host.empty());
CHECK(host.data() == nullptr);
CHECK(path.empty());
CHECK(path.data() == nullptr);
CHECK(query.empty());
CHECK(query.data() == nullptr);
CHECK(fragment.empty());
CHECK(fragment.data() == nullptr);
heap->destroy();
}