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