diff --git a/httplib.h b/httplib.h index f2916bd7..6bd261e8 100644 --- a/httplib.h +++ b/httplib.h @@ -10902,31 +10902,51 @@ inline std::pair make_digest_authentication_header( return std::make_pair(key, field); } +// Follows X509_check_host, which the OpenSSL backend uses, so that every +// backend accepts the same names. inline bool match_hostname(const std::string &pattern, const std::string &hostname) { + // A trailing dot on the host is ignored + auto host = hostname; + if (host.size() > 1 && host.back() == '.') { host.pop_back(); } + // Exact match (case-insensitive) - if (detail::case_ignore::equal(hostname, pattern)) { return true; } + if (detail::case_ignore::equal(host, pattern)) { return true; } - // Split both pattern and hostname into components by '.' - std::vector pattern_components; - if (!pattern.empty()) { - split(pattern.data(), pattern.data() + pattern.size(), '.', - [&](const char *b, const char *e) { - pattern_components.emplace_back(b, e); - }); - } - - std::vector host_components; - if (!hostname.empty()) { - split(hostname.data(), hostname.data() + hostname.size(), '.', - [&](const char *b, const char *e) { - host_components.emplace_back(b, e); - }); + // Split both pattern and hostname into components by '.', keeping any empty + // one, which no name may have + auto labels = [](const std::string &s) { + std::vector v; + size_t b = 0; + while (true) { + auto e = s.find('.', b); + v.push_back(s.substr(b, e - b)); + if (e == std::string::npos) { return v; } + b = e + 1; + } + }; + auto pattern_components = labels(pattern); + auto host_components = labels(host); + auto has_empty_label = [](const std::vector &v) { + return std::any_of(v.begin(), v.end(), + [](const std::string &l) { return l.empty(); }); + }; + if (has_empty_label(pattern_components) || has_empty_label(host_components)) { + return false; } // Component count must match if (host_components.size() != pattern_components.size()) { return false; } + // A wildcard needs two labels after it, so that "*.com" matches nothing, + // and an IDNA label may not have one + const auto &leftmost = pattern_components.front(); + if (leftmost.find('*') != std::string::npos && + (pattern_components.size() < 3 || + case_ignore::equal(leftmost.substr(0, 4), "xn--"))) { + return false; + } + // Compare each component with wildcard support // Supports: "*" (full wildcard), "prefix*" (partial wildcard) // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484 diff --git a/test/test.cc b/test/test.cc index 58d0b7a7..589d4b18 100644 --- a/test/test.cc +++ b/test/test.cc @@ -562,6 +562,9 @@ find_content_type(const std::string &path, const std::string &default_content_type); bool can_compress_content_type(const std::string &content_type); bool is_prohibited_header_name(const std::string &name); +#ifdef CPPHTTPLIB_SSL_ENABLED +bool match_hostname(const std::string &pattern, const std::string &hostname); +#endif } // namespace detail } // namespace httplib @@ -645,6 +648,26 @@ TEST(FileExtensionTest, TakesTheAlphanumericSuffix) { EXPECT_EQ(long_ext, detail::file_extension("a." + long_ext)); } +#ifdef CPPHTTPLIB_SSL_ENABLED +TEST(MatchHostnameTest, FollowsX509CheckHost) { + EXPECT_TRUE(detail::match_hostname("*.example.com", "www.example.com")); + EXPECT_TRUE(detail::match_hostname("www*.example.com", "www1.example.com")); + EXPECT_TRUE(detail::match_hostname("www.example.com", "WWW.example.com.")); + + // A wildcard needs two labels after it, in the leftmost label only + EXPECT_FALSE(detail::match_hostname("*.com", "example.com")); + EXPECT_FALSE(detail::match_hostname("*", "localhost")); + EXPECT_FALSE(detail::match_hostname("vic*.test", "victim.test")); + EXPECT_FALSE(detail::match_hostname("www.*.com", "www.a.com")); + EXPECT_FALSE( + detail::match_hostname("xn--*.example.com", "xn--a.example.com")); + + // No name may have an empty label + EXPECT_FALSE(detail::match_hostname("*.example.com", "a..example.com")); + EXPECT_FALSE(detail::match_hostname("a..example.com", "a.example.com")); +} +#endif + TEST(CaseIgnoreHashTest, IgnoresCaseAndTakesLongKeys) { detail::case_ignore::hash hash; EXPECT_EQ(hash("Content-Type"), hash("content-TYPE"));