| /** @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(); |
| } |