text: // Copyright 2018 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "services/network/public/cpp/cors/cors.h"
  
  #include <limits.h>
  
  #include "base/test/metrics/histogram_tester.h"
  #include "build/build_config.h"
  #include "testing/gtest/include/gtest/gtest.h"
  #include "url/gurl.h"
  #include "url/origin.h"
  
  namespace network {
  namespace cors {
  namespace {
  
  using CorsTest = testing::Test;
  
  // Tests if CheckAccess detects kWildcardOriginNotAllowed error correctly.
  TEST_F(CorsTest, CheckAccessDetectsWildcardOriginNotAllowed) {
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("http://google.com"));
    const std::string allow_all_header("*");
  
    // Access-Control-Allow-Origin '*' works.
    absl::optional<CorsErrorStatus> error1 =
        CheckAccess(response_url, allow_all_header /* allow_origin_header */,
                    absl::nullopt /* allow_credentials_header */,
                    network::mojom::CredentialsMode::kOmit, origin);
    EXPECT_FALSE(error1);
  
    // Access-Control-Allow-Origin '*' should not be allowed if credentials mode
    // is kInclude.
    absl::optional<CorsErrorStatus> error2 =
        CheckAccess(response_url, allow_all_header /* allow_origin_header */,
                    absl::nullopt /* allow_credentials_header */,
                    network::mojom::CredentialsMode::kInclude, origin);
    ASSERT_TRUE(error2);
    EXPECT_EQ(mojom::CorsError::kWildcardOriginNotAllowed, error2->cors_error);
  }
  
  // Tests if CheckAccess detects kMissingAllowOriginHeader error correctly.
  TEST_F(CorsTest, CheckAccessDetectsMissingAllowOriginHeader) {
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("http://google.com"));
  
    // Access-Control-Allow-Origin is missed.
    absl::optional<CorsErrorStatus> error =
        CheckAccess(response_url, absl::nullopt /* allow_origin_header */,
                    absl::nullopt /* allow_credentials_header */,
                    network::mojom::CredentialsMode::kOmit, origin);
    ASSERT_TRUE(error);
    EXPECT_EQ(mojom::CorsError::kMissingAllowOriginHeader, error->cors_error);
  }
  
  // Tests if CheckAccess detects kMultipleAllowOriginValues error
  // correctly.
  TEST_F(CorsTest, CheckAccessDetectsMultipleAllowOriginValues) {
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("http://google.com"));
  
    const std::string space_separated_multiple_origins(
        "http://example.com http://another.example.com");
    absl::optional<CorsErrorStatus> error1 = CheckAccess(
        response_url, space_separated_multiple_origins /* allow_origin_header */,
        absl::nullopt /* allow_credentials_header */,
        network::mojom::CredentialsMode::kOmit, origin);
    ASSERT_TRUE(error1);
    EXPECT_EQ(mojom::CorsError::kMultipleAllowOriginValues, error1->cors_error);
  
    const std::string comma_separated_multiple_origins(
        "http://example.com,http://another.example.com");
    absl::optional<CorsErrorStatus> error2 = CheckAccess(
        response_url, comma_separated_multiple_origins /* allow_origin_header */,
        absl::nullopt /* allow_credentials_header */,
        network::mojom::CredentialsMode::kOmit, origin);
    ASSERT_TRUE(error2);
    EXPECT_EQ(mojom::CorsError::kMultipleAllowOriginValues, error2->cors_error);
  }
  
  // Tests if CheckAccess detects kInvalidAllowOriginValue error correctly.
  TEST_F(CorsTest, CheckAccessDetectsInvalidAllowOriginValue) {
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("http://google.com"));
  
    absl::optional<CorsErrorStatus> error = CheckAccess(
        response_url, std::string("invalid.origin") /* allow_origin_header */,
        absl::nullopt /* allow_credentials_header */,
        network::mojom::CredentialsMode::kOmit, origin);
    ASSERT_TRUE(error);
    EXPECT_EQ(mojom::CorsError::kInvalidAllowOriginValue, error->cors_error);
    EXPECT_EQ("invalid.origin", error->failed_parameter);
  }
  
  // Tests if CheckAccess detects kAllowOriginMismatch error correctly.
  TEST_F(CorsTest, CheckAccessDetectsAllowOriginMismatch) {
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("http://google.com"));
  
    absl::optional<CorsErrorStatus> error1 =
        CheckAccess(response_url, origin.Serialize() /* allow_origin_header */,
                    absl::nullopt /* allow_credentials_header */,
                    network::mojom::CredentialsMode::kOmit, origin);
    ASSERT_FALSE(error1);
  
    absl::optional<CorsErrorStatus> error2 = CheckAccess(
        response_url,
        std::string("http://not.google.com") /* allow_origin_header */,
        absl::nullopt /* allow_credentials_header */,
        network::mojom::CredentialsMode::kOmit, origin);
    ASSERT_TRUE(error2);
    EXPECT_EQ(mojom::CorsError::kAllowOriginMismatch, error2->cors_error);
    EXPECT_EQ("http://not.google.com", error2->failed_parameter);
  
    // Allow "null" value to match serialized unique origins.
    const std::string null_string("null");
    const url::Origin null_origin;
    EXPECT_EQ(null_string, null_origin.Serialize());
  
    absl::optional<CorsErrorStatus> error3 =
        CheckAccess(response_url, null_string /* allow_origin_header */,
                    absl::nullopt /* allow_credentials_header */,
                    network::mojom::CredentialsMode::kOmit, null_origin);
    EXPECT_FALSE(error3);
  }
  
  // Tests if CheckAccess detects kInvalidAllowCredentials error correctly.
  TEST_F(CorsTest, CheckAccessDetectsInvalidAllowCredential) {
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("http://google.com"));
  
    absl::optional<CorsErrorStatus> error1 =
        CheckAccess(response_url, origin.Serialize() /* allow_origin_header */,
                    std::string("true") /* allow_credentials_header */,
                    network::mojom::CredentialsMode::kInclude, origin);
    ASSERT_FALSE(error1);
  
    absl::optional<CorsErrorStatus> error2 =
        CheckAccess(response_url, origin.Serialize() /* allow_origin_header */,
                    std::string("fuga") /* allow_credentials_header */,
                    network::mojom::CredentialsMode::kInclude, origin);
    ASSERT_TRUE(error2);
    EXPECT_EQ(mojom::CorsError::kInvalidAllowCredentials, error2->cors_error);
    EXPECT_EQ("fuga", error2->failed_parameter);
  }
  
  // Should match unexposed enum in cors.cc
  enum class AccessCheckResult {
    kPermitted = 0,
    kNotPermitted = 1,
    kPermittedInPreflight = 2,
    kNotPermittedInPreflight = 3,
  
    kMaxValue = kNotPermittedInPreflight,
  };
  constexpr char kAccessCheckHistogram[] = "Net.Cors.AccessCheckResult";
  constexpr char kAccessCheckHistogramNotSecure[] =
      "Net.Cors.AccessCheckResult.NotSecureRequestor";
  
  TEST_F(CorsTest, CheckAccessLogsAllowMetricsSecureOrigin) {
    base::HistogramTester histogram_tester;
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("https://google.com"));
  
    CheckAccess(response_url, origin.Serialize() /* allow_origin_header */,
                absl::nullopt /* allow_credentials_header */,
                network::mojom::CredentialsMode::kOmit, origin);
    histogram_tester.ExpectUniqueSample(kAccessCheckHistogram,
                                        AccessCheckResult::kPermitted, 1);
    histogram_tester.ExpectTotalCount(kAccessCheckHistogramNotSecure, 0);
  }
  
  TEST_F(CorsTest, CheckAccessLogsBlockMetricsSecureOrigin) {
    base::HistogramTester histogram_tester;
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("https://google.com"));
  
    CheckAccess(response_url,
                std::string("https://not.google.com") /* allow_origin_header */,
                absl::nullopt /* allow_credentials_header */,
                network::mojom::CredentialsMode::kOmit, origin);
    histogram_tester.ExpectUniqueSample(kAccessCheckHistogram,
                                        AccessCheckResult::kNotPermitted, 1);
    histogram_tester.ExpectTotalCount(kAccessCheckHistogramNotSecure, 0);
  }
  
  TEST_F(CorsTest, CheckAccessLogsAllowMetricsInsecureOrigin) {
    base::HistogramTester histogram_tester;
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("http://google.com"));
  
    CheckAccess(response_url, origin.Serialize() /* allow_origin_header */,
                absl::nullopt /* allow_credentials_header */,
                network::mojom::CredentialsMode::kOmit, origin);
    histogram_tester.ExpectUniqueSample(kAccessCheckHistogram,
                                        AccessCheckResult::kPermitted, 1);
    histogram_tester.ExpectUniqueSample(kAccessCheckHistogramNotSecure,
                                        AccessCheckResult::kPermitted, 1);
  }
  
  TEST_F(CorsTest, CheckAccessLogsBlockMetricsInsecureOrigin) {
    base::HistogramTester histogram_tester;
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("http://google.com"));
  
    CheckAccess(response_url,
                std::string("http://not.google.com") /* allow_origin_header */,
                absl::nullopt /* allow_credentials_header */,
                network::mojom::CredentialsMode::kOmit, origin);
    histogram_tester.ExpectUniqueSample(kAccessCheckHistogram,
                                        AccessCheckResult::kNotPermitted, 1);
    histogram_tester.ExpectUniqueSample(kAccessCheckHistogramNotSecure,
                                        AccessCheckResult::kNotPermitted, 1);
  }
  
  // Tests if CheckRedirectLocation detects kCorsDisabledScheme and
  // kRedirectContainsCredentials errors correctly.
  TEST_F(CorsTest, CheckRedirectLocation) {
    struct TestCase {
      GURL url;
      mojom::RequestMode request_mode;
      bool cors_flag;
      bool tainted;
      absl::optional<CorsErrorStatus> expectation;
    };
  
    const auto kCors = mojom::RequestMode::kCors;
    const auto kCorsWithForcedPreflight =
        mojom::RequestMode::kCorsWithForcedPreflight;
    const auto kNoCors = mojom::RequestMode::kNoCors;
  
    const url::Origin origin = url::Origin::Create(GURL("http://example.com/"));
    const GURL same_origin_url("http://example.com/");
    const GURL cross_origin_url("http://example2.com/");
    const GURL data_url("data:,Hello");
    const GURL same_origin_url_with_user("http://yukari@example.com/");
    const GURL same_origin_url_with_pass("http://:tamura@example.com/");
    const GURL cross_origin_url_with_user("http://yukari@example2.com/");
    const GURL cross_origin_url_with_pass("http://:tamura@example2.com/");
    const auto ok = absl::nullopt;
    const CorsErrorStatus kCorsDisabledScheme(
        mojom::CorsError::kCorsDisabledScheme);
    const CorsErrorStatus kRedirectContainsCredentials(
        mojom::CorsError::kRedirectContainsCredentials);
  
    TestCase cases[] = {
        // "cors", no credentials information
        {same_origin_url, kCors, false, false, ok},
        {cross_origin_url, kCors, false, false, ok},
        {data_url, kCors, false, false, ok},
        {same_origin_url, kCors, true, false, ok},
        {cross_origin_url, kCors, true, false, ok},
        {data_url, kCors, true, false, ok},
        {same_origin_url, kCors, false, true, ok},
        {cross_origin_url, kCors, false, true, ok},
        {data_url, kCors, false, true, ok},
        {same_origin_url, kCors, true, true, ok},
        {cross_origin_url, kCors, true, true, ok},
        {data_url, kCors, true, true, ok},
  
        // "cors" with forced preflight, no credentials information
        {same_origin_url, kCorsWithForcedPreflight, false, false, ok},
        {cross_origin_url, kCorsWithForcedPreflight, false, false, ok},
        {data_url, kCorsWithForcedPreflight, false, false, ok},
        {same_origin_url, kCorsWithForcedPreflight, true, false, ok},
        {cross_origin_url, kCorsWithForcedPreflight, true, false, ok},
        {data_url, kCorsWithForcedPreflight, true, false, ok},
        {same_origin_url, kCorsWithForcedPreflight, false, true, ok},
        {cross_origin_url, kCorsWithForcedPreflight, false, true, ok},
        {data_url, kCorsWithForcedPreflight, false, true, ok},
        {same_origin_url, kCorsWithForcedPreflight, true, true, ok},
        {cross_origin_url, kCorsWithForcedPreflight, true, true, ok},
        {data_url, kCorsWithForcedPreflight, true, true, ok},
  
        // "no-cors", no credentials information
        {same_origin_url, kNoCors, false, false, ok},
        {cross_origin_url, kNoCors, false, false, ok},
        {data_url, kNoCors, false, false, ok},
        {same_origin_url, kNoCors, false, true, ok},
        {cross_origin_url, kNoCors, false, true, ok},
        {data_url, kNoCors, false, true, ok},
  
        // with credentials information (same origin)
        {same_origin_url_with_user, kCors, false, false, ok},
        {same_origin_url_with_user, kCors, true, false,
         kRedirectContainsCredentials},
        {same_origin_url_with_user, kCors, true, true,
         kRedirectContainsCredentials},
        {same_origin_url_with_user, kNoCors, false, false, ok},
        {same_origin_url_with_user, kNoCors, false, true, ok},
        {same_origin_url_with_pass, kCors, false, false, ok},
        {same_origin_url_with_pass, kCors, true, false,
         kRedirectContainsCredentials},
        {same_origin_url_with_pass, kCors, true, true,
         kRedirectContainsCredentials},
        {same_origin_url_with_pass, kNoCors, false, false, ok},
        {same_origin_url_with_pass, kNoCors, false, true, ok},
  
        // with credentials information (cross origin)
        {cross_origin_url_with_user, kCors, false, false,
         kRedirectContainsCredentials},
        {cross_origin_url_with_user, kCors, true, false,
         kRedirectContainsCredentials},
        {cross_origin_url_with_user, kCors, true, true,
         kRedirectContainsCredentials},
        {cross_origin_url_with_user, kNoCors, false, true, ok},
        {cross_origin_url_with_user, kNoCors, false, false, ok},
        {cross_origin_url_with_pass, kCors, false, false,
         kRedirectContainsCredentials},
        {cross_origin_url_with_pass, kCors, true, false,
         kRedirectContainsCredentials},
        {cross_origin_url_with_pass, kCors, true, true,
         kRedirectContainsCredentials},
        {cross_origin_url_with_pass, kNoCors, false, true, ok},
        {cross_origin_url_with_pass, kNoCors, false, false, ok},
    };
  
    for (const auto& test : cases) {
      SCOPED_TRACE(testing::Message()
                   << "url: " << test.url
                   << ", request mode: " << test.request_mode
                   << ", origin: " << origin << ", cors_flag: " << test.cors_flag
                   << ", tainted: " << test.tainted);
  
      EXPECT_EQ(test.expectation,
                CheckRedirectLocation(test.url, test.request_mode, origin,
                                      test.cors_flag, test.tainted));
    }
  }
  
  TEST_F(CorsTest, CheckPreflightAccessDetectsErrorStatus) {
    const GURL response_url("http://example.com/data");
    const url::Origin origin = url::Origin::Create(GURL("http://google.com"));
    const std::string allow_all_header("*");
  
    // Status 200-299 should pass.
    EXPECT_FALSE(
        CheckPreflightAccess(response_url, 200, allow_all_header,
                             absl::nullopt /* allow_credentials_header */,
                             network::mojom::CredentialsMode::kOmit, origin));
    EXPECT_FALSE(
        CheckPreflightAccess(response_url, 299, allow_all_header,
                             absl::nullopt /* allow_credentials_header */,
                             network::mojom::CredentialsMode::kOmit, origin));
  
    // Status 300 should fail.
    absl::optional<CorsErrorStatus> invalid_status_error =
        CheckPreflightAccess(response_url, 300, allow_all_header,
                             absl::nullopt /* allow_credentials_header */,
                             network::mojom::CredentialsMode::kOmit, origin);
    ASSERT_TRUE(invalid_status_error);
    EXPECT_EQ(mojom::CorsError::kPreflightInvalidStatus,
              invalid_status_error->cors_error);
  
    // Status 0 should fail too.
    invalid_status_error =
        CheckPreflightAccess(response_url, 0, allow_all_header,
                             absl::nullopt /* allow_credentials_header */,
                             network::mojom::CredentialsMode::kOmit, origin);
    ASSERT_TRUE(invalid_status_error);
    EXPECT_EQ(mojom::CorsError::kPreflightInvalidStatus,
              invalid_status_error->cors_error);
  }
  
  TEST_F(CorsTest, CheckExternalPreflightErrors) {
    EXPECT_FALSE(CheckExternalPreflight(std::string("true")));
  
    absl::optional<CorsErrorStatus> error2 =
        CheckExternalPreflight(absl::nullopt);
    ASSERT_TRUE(error2);
    EXPECT_EQ(mojom::CorsError::kPreflightMissingAllowExternal,
              error2->cors_error);
    EXPECT_EQ("", error2->failed_parameter);
  
    absl::optional<CorsErrorStatus> error3 =
        CheckExternalPreflight(std::string("TRUE"));
    ASSERT_TRUE(error3);
    EXPECT_EQ(mojom::CorsError::kPreflightInvalidAllowExternal,
              error3->cors_error);
    EXPECT_EQ("TRUE", error3->failed_parameter);
  }
  
  TEST_F(CorsTest, SafelistedMethod) {
    // Method check should be case-insensitive.
    EXPECT_TRUE(IsCorsSafelistedMethod("get"));
    EXPECT_TRUE(IsCorsSafelistedMethod("Get"));
    EXPECT_TRUE(IsCorsSafelistedMethod("GET"));
    EXPECT_TRUE(IsCorsSafelistedMethod("HEAD"));
    EXPECT_TRUE(IsCorsSafelistedMethod("POST"));
    EXPECT_FALSE(IsCorsSafelistedMethod("OPTIONS"));
  }
  
  TEST_F(CorsTest, SafelistedHeader) {
    // See SafelistedAccept/AcceptLanguage/ContentLanguage/ContentType also.
  
    EXPECT_TRUE(IsCorsSafelistedHeader("accept", "foo"));
    EXPECT_FALSE(IsCorsSafelistedHeader("foo", "bar"));
    EXPECT_FALSE(IsCorsSafelistedHeader("user-agent", "foo"));
  }
  
  TEST_F(CorsTest, SafelistedAccept) {
    EXPECT_TRUE(IsCorsSafelistedHeader("accept", "text/html"));
    EXPECT_TRUE(IsCorsSafelistedHeader("AccepT", "text/html"));
  
    constexpr char kAllowed[] =
        "\t !#$%&'*+,-./0123456789;="
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~";
    for (int i = 0; i < 128; ++i) {
      SCOPED_TRACE(testing::Message() << "c = static_cast<char>(" << i << ")");
      char c = static_cast<char>(i);
      // 1 for the trailing null character.
      auto* end = kAllowed + base::size(kAllowed) - 1;
      EXPECT_EQ(std::find(kAllowed, end, c) != end,
                IsCorsSafelistedHeader("accept", std::string(1, c)));
      EXPECT_EQ(std::find(kAllowed, end, c) != end,
                IsCorsSafelistedHeader("AccepT", std::string(1, c)));
    }
    for (int i = 128; i <= 255; ++i) {
      SCOPED_TRACE(testing::Message() << "c = static_cast<char>(" << i << ")");
      char c = static_cast<char>(static_cast<unsigned char>(i));
      EXPECT_TRUE(IsCorsSafelistedHeader("accept", std::string(1, c)));
      EXPECT_TRUE(IsCorsSafelistedHeader("AccepT", std::string(1, c)));
    }
  
    EXPECT_TRUE(IsCorsSafelistedHeader("accept", std::string(128, 'a')));
    EXPECT_FALSE(IsCorsSafelistedHeader("accept", std::string(129, 'a')));
    EXPECT_TRUE(IsCorsSafelistedHeader("AccepT", std::string(128, 'a')));
    EXPECT_FALSE(IsCorsSafelistedHeader("AccepT", std::string(129, 'a')));
  }
  
  TEST_F(CorsTest, SafelistedAcceptLanguage) {
    EXPECT_TRUE(IsCorsSafelistedHeader("accept-language", "en,ja"));
    EXPECT_TRUE(IsCorsSafelistedHeader("aCcEPT-lAngUAge", "en,ja"));
  
    constexpr char kAllowed[] =
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz *,-.;=";
    for (int i = CHAR_MIN; i <= CHAR_MAX; ++i) {
      SCOPED_TRACE(testing::Message() << "c = static_cast<char>(" << i << ")");
      char c = static_cast<char>(i);
      // 1 for the trailing null character.
      auto* end = kAllowed + base::size(kAllowed) - 1;
      EXPECT_EQ(std::find(kAllowed, end, c) != end,
                IsCorsSafelistedHeader("aCcEPT-lAngUAge", std::string(1, c)));
    }
    EXPECT_TRUE(IsCorsSafelistedHeader("accept-language", std::string(128, 'a')));
    EXPECT_FALSE(
        IsCorsSafelistedHeader("accept-language", std::string(129, 'a')));
    EXPECT_TRUE(IsCorsSafelistedHeader("aCcEPT-lAngUAge", std::string(128, 'a')));
    EXPECT_FALSE(
        IsCorsSafelistedHeader("aCcEPT-lAngUAge", std::string(129, 'a')));
  }
  
  TEST_F(CorsTest, SafelistedSecCHPrefersColorScheme) {
    EXPECT_TRUE(IsCorsSafelistedHeader("Sec-CH-Prefers-Color-Scheme",
                                       "\"Prefers-Color-Scheme!\""));
  }
  
  TEST_F(CorsTest, SafelistedSecCHLang) {
    EXPECT_TRUE(IsCorsSafelistedHeader("Sec-CH-Lang", "\"en\", \"de\""));
  
    // TODO(mkwst): Validate that `Sec-CH-Lang` is a structured header.
    // https://crbug.com/924969
  }
  
  TEST_F(CorsTest, SafelistedSecCHUA) {
    EXPECT_TRUE(IsCorsSafelistedHeader("Sec-CH-UA", "\"User Agent!\""));
    EXPECT_TRUE(IsCorsSafelistedHeader("Sec-CH-UA-Platform", "\"Platform!\""));
    EXPECT_TRUE(IsCorsSafelistedHeader("Sec-CH-UA-Platform-Version",
                                       "\"Platform-Version!\""));
    EXPECT_TRUE(IsCorsSafelistedHeader("Sec-CH-UA-Arch", "\"Architecture!\""));
    EXPECT_TRUE(IsCorsSafelistedHeader("Sec-CH-UA-Model", "\"Model!\""));
  
    // TODO(mkwst): Validate that `Sec-CH-UA-*` is a structured header.
    // https://crbug.com/924969
  }
  
  TEST_F(CorsTest, SafelistedContentLanguage) {
    EXPECT_TRUE(IsCorsSafelistedHeader("content-language", "en,ja"));
    EXPECT_TRUE(IsCorsSafelistedHeader("cONTent-LANguaGe", "en,ja"));
  
    constexpr char kAllowed[] =
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz *,-.;=";
    for (int i = CHAR_MIN; i <= CHAR_MAX; ++i) {
      SCOPED_TRACE(testing::Message() << "c = static_cast<char>(" << i << ")");
      char c = static_cast<char>(i);
      // 1 for the trailing null character.
      auto* end = kAllowed + base::size(kAllowed) - 1;
      EXPECT_EQ(std::find(kAllowed, end, c) != end,
                IsCorsSafelistedHeader("content-language", std::string(1, c)));
      EXPECT_EQ(std::find(kAllowed, end, c) != end,
                IsCorsSafelistedHeader("cONTent-LANguaGe", std::string(1, c)));
    }
    EXPECT_TRUE(
        IsCorsSafelistedHeader("content-language", std::string(128, 'a')));
    EXPECT_FALSE(
        IsCorsSafelistedHeader("content-language", std::string(129, 'a')));
    EXPECT_TRUE(
        IsCorsSafelistedHeader("cONTent-LANguaGe", std::string(128, 'a')));
    EXPECT_FALSE(
        IsCorsSafelistedHeader("cONTent-LANguaGe", std::string(129, 'a')));
  }
  
  TEST_F(CorsTest, SafelistedContentType) {
    constexpr char kAllowed[] =
        "\t !#$%&'*+,-./0123456789;="
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~";
    for (int i = 0; i < 128; ++i) {
      SCOPED_TRACE(testing::Message() << "c = static_cast<char>(" << i << ")");
      const char c = static_cast<char>(i);
      // 1 for the trailing null character.
      const auto* const end = kAllowed + base::size(kAllowed) - 1;
      const bool is_allowed = std::find(kAllowed, end, c) != end;
      const std::string value = std::string("text/plain; charset=") + c;
  
      EXPECT_EQ(is_allowed, IsCorsSafelistedHeader("content-type", value));
      EXPECT_EQ(is_allowed, IsCorsSafelistedHeader("cONtent-tYPe", value));
    }
    for (int i = 128; i <= 255; ++i) {
      SCOPED_TRACE(testing::Message() << "c = static_cast<char>(" << i << ")");
      char c = static_cast<char>(static_cast<unsigned char>(i));
      const std::string value = std::string("text/plain; charset=") + c;
      EXPECT_TRUE(IsCorsSafelistedHeader("content-type", value));
      EXPECT_TRUE(IsCorsSafelistedHeader("ConTEnt-Type", value));
    }
  
    EXPECT_TRUE(IsCorsSafelistedHeader("content-type", "text/plain"));
    EXPECT_TRUE(IsCorsSafelistedHeader("CoNtEnt-TyPE", "text/plain"));
    EXPECT_TRUE(
        IsCorsSafelistedHeader("content-type", "text/plain; charset=utf-8"));
    EXPECT_TRUE(
        IsCorsSafelistedHeader("content-type", "  text/plain ; charset=UTF-8"));
    EXPECT_TRUE(
        IsCorsSafelistedHeader("content-type", "text/plain; param=BOGUS"));
    EXPECT_TRUE(IsCorsSafelistedHeader("content-type",
                                       "application/x-www-form-urlencoded"));
    EXPECT_TRUE(IsCorsSafelistedHeader("content-type", "multipart/form-data"));
  
    EXPECT_TRUE(IsCorsSafelistedHeader("content-type", "Text/plain"));
    EXPECT_TRUE(IsCorsSafelistedHeader("content-type", "tEXT/PLAIN"));
    EXPECT_FALSE(IsCorsSafelistedHeader("content-type", "text/html"));
    EXPECT_FALSE(IsCorsSafelistedHeader("CoNtEnt-TyPE", "text/html"));
  
    EXPECT_FALSE(IsCorsSafelistedHeader("content-type", "image/png"));
    EXPECT_FALSE(IsCorsSafelistedHeader("CoNtEnt-TyPE", "image/png"));
    EXPECT_TRUE(IsCorsSafelistedHeader(
        "content-type", "text/plain; charset=" + std::string(108, 'a')));
    EXPECT_TRUE(IsCorsSafelistedHeader(
        "cONTent-tYPE", "text/plain; charset=" + std::string(108, 'a')));
    EXPECT_FALSE(IsCorsSafelistedHeader(
        "content-type", "text/plain; charset=" + std::string(109, 'a')));
    EXPECT_FALSE(IsCorsSafelistedHeader(
        "cONTent-tYPE", "text/plain; charset=" + std::string(109, 'a')));
  }
  
  TEST_F(CorsTest, CheckCorsClientHintsSafelist) {
    EXPECT_FALSE(IsCorsSafelistedHeader("device-memory", ""));
    EXPECT_FALSE(IsCorsSafelistedHeader("device-memory", "abc"));
    EXPECT_TRUE(IsCorsSafelistedHeader("device-memory", "1.25"));
    EXPECT_TRUE(IsCorsSafelistedHeader("DEVICE-memory", "1.25"));
    EXPECT_FALSE(IsCorsSafelistedHeader("device-memory", "1.25-2.5"));
    EXPECT_FALSE(IsCorsSafelistedHeader("device-memory", "-1.25"));
    EXPECT_FALSE(IsCorsSafelistedHeader("device-memory", "1e2"));
    EXPECT_FALSE(IsCorsSafelistedHeader("device-memory", "inf"));
    EXPECT_FALSE(IsCorsSafelistedHeader("device-memory", "-2.3"));
    EXPECT_FALSE(IsCorsSafelistedHeader("device-memory", "NaN"));
    EXPECT_FALSE(IsCorsSafelistedHeader("DEVICE-memory", "1.25.3"));
    EXPECT_FALSE(IsCorsSafelistedHeader("DEVICE-memory", "1."));
    EXPECT_FALSE(IsCorsSafelistedHeader("DEVICE-memory", ".1"));
    EXPECT_FALSE(IsCorsSafelistedHeader("DEVICE-memory", "."));
    EXPECT_TRUE(IsCorsSafelistedHeader("DEVICE-memory", "1"));
  
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", ""));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", "abc"));
    EXPECT_TRUE(IsCorsSafelistedHeader("dpr", "1.25"));
    EXPECT_TRUE(IsCorsSafelistedHeader("Dpr", "1.25"));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", "1.25-2.5"));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", "-1.25"));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", "1e2"));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", "inf"));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", "-2.3"));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", "NaN"));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", "1.25.3"));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", "1."));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", ".1"));
    EXPECT_FALSE(IsCorsSafelistedHeader("dpr", "."));
    EXPECT_TRUE(IsCorsSafelistedHeader("dpr", "1"));
  
    EXPECT_FALSE(IsCorsSafelistedHeader("width", ""));
    EXPECT_FALSE(IsCorsSafelistedHeader("width", "abc"));
    EXPECT_TRUE(IsCorsSafelistedHeader("width", "125"));
    EXPECT_TRUE(IsCorsSafelistedHeader("width", "1"));
    EXPECT_TRUE(IsCorsSafelistedHeader("WIDTH", "125"));
    EXPECT_FALSE(IsCorsSafelistedHeader("width", "125.2"));
    EXPECT_FALSE(IsCorsSafelistedHeader("width", "-125"));
    EXPECT_TRUE(IsCorsSafelistedHeader("width", "2147483648"));
  
    EXPECT_FALSE(IsCorsSafelistedHeader("viewport-width", ""));
    EXPECT_FALSE(IsCorsSafelistedHeader("viewport-width", "abc"));
    EXPECT_TRUE(IsCorsSafelistedHeader("viewport-width", "125"));
    EXPECT_TRUE(IsCorsSafelistedHeader("viewport-width", "1"));
    EXPECT_TRUE(IsCorsSafelistedHeader("viewport-Width", "125"));
    EXPECT_FALSE(IsCorsSafelistedHeader("viewport-width", "125.2"));
    EXPECT_TRUE(IsCorsSafelistedHeader("viewport-width", "2147483648"));
  }
  
  TEST_F(CorsTest, CorsUnsafeRequestHeaderNames) {
    // Needed because initializer list is not allowed for a macro argument.
    using List = std::vector<std::string>;
  
    // Empty => Empty
    EXPECT_EQ(CorsUnsafeRequestHeaderNames({}), List({}));
  
    // Some headers are safelisted.
    EXPECT_EQ(CorsUnsafeRequestHeaderNames({{"content-type", "text/plain"},
                                            {"dpr", "12345"},
                                            {"aCCept", "en,ja"},
                                            {"accept-charset", "utf-8"},
                                            {"uSer-Agent", "foo"},
                                            {"hogE", "fuga"}}),
              List({"accept-charset", "user-agent", "hoge"}));
  
    // All headers are not safelisted.
    EXPECT_EQ(
        CorsUnsafeRequestHeaderNames({{"content-type", "text/html"},
                                      {"dpr", "123-45"},
                                      {"aCCept", "en,ja"},
                                      {"accept-charset", "utf-8"},
                                      {"uSer-Agent", "foo"},
                                      {"hogE", "fuga"}}),
        List({"content-type", "dpr", "accept-charset", "user-agent", "hoge"}));
  
    // |safelistValueSize| is 1024.
    EXPECT_EQ(
        CorsUnsafeRequestHeaderNames(
            {{"content-type", "text/plain; charset=" + std::string(108, '1')},
             {"accept", std::string(128, '1')},
             {"accept-language", std::string(128, '1')},
             {"content-language", std::string(128, '1')},
             {"dpr", std::string(128, '1')},
             {"device-memory", std::string(128, '1')},
             {"save-data", "on"},
             {"viewport-width", std::string(128, '1')},
             {"width", std::string(126, '1')},
             {"hogE", "fuga"}}),
        List({"hoge"}));
  
    // |safelistValueSize| is 1025.
    EXPECT_EQ(
        CorsUnsafeRequestHeaderNames(
            {{"content-type", "text/plain; charset=" + std::string(108, '1')},
             {"accept", std::string(128, '1')},
             {"accept-language", std::string(128, '1')},
             {"content-language", std::string(128, '1')},
             {"dpr", std::string(128, '1')},
             {"device-memory", std::string(128, '1')},
             {"save-data", "on"},
             {"viewport-width", std::string(128, '1')},
             {"width", std::string(127, '1')},
             {"hogE", "fuga"}}),
        List({"hoge", "content-type", "accept", "accept-language",
              "content-language", "dpr", "device-memory", "save-data",
              "viewport-width", "width"}));
  
    // |safelistValueSize| is 897 because "content-type" is not safelisted.
    EXPECT_EQ(
        CorsUnsafeRequestHeaderNames(
            {{"content-type", "text/plain; charset=" + std::string(128, '1')},
             {"accept", std::string(128, '1')},
             {"accept-language", std::string(128, '1')},
             {"content-language", std::string(128, '1')},
             {"dpr", std::string(128, '1')},
             {"device-memory", std::string(128, '1')},
             {"save-data", "on"},
             {"viewport-width", std::string(128, '1')},
             {"width", std::string(127, '1')},
             {"hogE", "fuga"}}),
        List({"content-type", "hoge"}));
  }
  
  TEST_F(CorsTest, CorsUnsafeNotForbiddenRequestHeaderNames) {
    // Needed because initializer list is not allowed for a macro argument.
    using List = std::vector<std::string>;
  
    // Empty => Empty
    EXPECT_EQ(
        CorsUnsafeNotForbiddenRequestHeaderNames({}, false /* is_revalidating */),
        List({}));
  
    // "user-agent" is NOT forbidden per spec, but forbidden in Chromium.
    EXPECT_EQ(
        CorsUnsafeNotForbiddenRequestHeaderNames({{"content-type", "text/plain"},
                                                  {"dpr", "12345"},
                                                  {"aCCept", "en,ja"},
                                                  {"accept-charset", "utf-8"},
                                                  {"uSer-Agent", "foo"},
                                                  {"hogE", "fuga"}},
                                                 false /* is_revalidating */),
        List({"hoge"}));
  
    EXPECT_EQ(
        CorsUnsafeNotForbiddenRequestHeaderNames({{"content-type", "text/html"},
                                                  {"dpr", "123-45"},
                                                  {"aCCept", "en,ja"},
                                                  {"accept-charset", "utf-8"},
                                                  {"hogE", "fuga"}},
                                                 false /* is_revalidating */),
        List({"content-type", "dpr", "hoge"}));
  
    // |safelistValueSize| is 1024.
    EXPECT_EQ(
        CorsUnsafeNotForbiddenRequestHeaderNames(
            {{"content-type", "text/plain; charset=" + std::string(108, '1')},
             {"accept", std::string(128, '1')},
             {"accept-language", std::string(128, '1')},
             {"content-language", std::string(128, '1')},
             {"dpr", std::string(128, '1')},
             {"device-memory", std::string(128, '1')},
             {"save-data", "on"},
             {"viewport-width", std::string(128, '1')},
             {"width", std::string(126, '1')},
             {"accept-charset", "utf-8"},
             {"hogE", "fuga"}},
            false /* is_revalidating */),
        List({"hoge"}));
  
    // |safelistValueSize| is 1025.
    EXPECT_EQ(
        CorsUnsafeNotForbiddenRequestHeaderNames(
            {{"content-type", "text/plain; charset=" + std::string(108, '1')},
             {"accept", std::string(128, '1')},
             {"accept-language", std::string(128, '1')},
             {"content-language", std::string(128, '1')},
             {"dpr", std::string(128, '1')},
             {"device-memory", std::string(128, '1')},
             {"save-data", "on"},
             {"viewport-width", std::string(128, '1')},
             {"width", std::string(127, '1')},
             {"accept-charset", "utf-8"},
             {"hogE", "fuga"}},
            false /* is_revalidating */),
        List({"hoge", "content-type", "accept", "accept-language",
              "content-language", "dpr", "device-memory", "save-data",
              "viewport-width", "width"}));
  
    // |safelistValueSize| is 897 because "content-type" is not safelisted.
    EXPECT_EQ(
        CorsUnsafeNotForbiddenRequestHeaderNames(
            {{"content-type", "text/plain; charset=" + std::string(128, '1')},
             {"accept", std::string(128, '1')},
             {"accept-language", std::string(128, '1')},
             {"content-language", std::string(128, '1')},
             {"dpr", std::string(128, '1')},
             {"device-memory", std::string(128, '1')},
             {"save-data", "on"},
             {"viewport-width", std::string(128, '1')},
             {"width", std::string(127, '1')},
             {"accept-charset", "utf-8"},
             {"hogE", "fuga"}},
            false /* is_revalidating */),
        List({"content-type", "hoge"}));
  }
  
  TEST_F(CorsTest, CorsUnsafeNotForbiddenRequestHeaderNamesWithRevalidating) {
    // Needed because initializer list is not allowed for a macro argument.
    using List = std::vector<std::string>;
  
    // Empty => Empty
    EXPECT_EQ(
        CorsUnsafeNotForbiddenRequestHeaderNames({}, true /* is_revalidating */),
        List({}));
  
    // These three headers will be ignored.
    EXPECT_EQ(
        CorsUnsafeNotForbiddenRequestHeaderNames({{"If-MODifIED-since", "x"},
                                                  {"iF-nONE-MATCh", "y"},
                                                  {"CACHE-ContrOl", "z"}},
                                                 true /* is_revalidating */),
        List({}));
  
    // Without is_revalidating set, these three headers will not be safelisted.
    EXPECT_EQ(
        CorsUnsafeNotForbiddenRequestHeaderNames({{"If-MODifIED-since", "x"},
                                                  {"iF-nONE-MATCh", "y"},
                                                  {"CACHE-ContrOl", "z"}},
                                                 false /* is_revalidating */),
        List({"if-modified-since", "if-none-match", "cache-control"}));
  }
  
  TEST_F(CorsTest, NoCorsSafelistedHeaderName) {
    EXPECT_TRUE(IsNoCorsSafelistedHeaderName("accept"));
    EXPECT_TRUE(IsNoCorsSafelistedHeaderName("AcCePT"));
    EXPECT_TRUE(IsNoCorsSafelistedHeaderName("accept-language"));
    EXPECT_TRUE(IsNoCorsSafelistedHeaderName("acCEPt-lAnguage"));
    EXPECT_TRUE(IsNoCorsSafelistedHeaderName("content-language"));
    EXPECT_TRUE(IsNoCorsSafelistedHeaderName("coNTENt-lAnguage"));
    EXPECT_TRUE(IsNoCorsSafelistedHeaderName("content-type"));
    EXPECT_TRUE(IsNoCorsSafelistedHeaderName("CONTENT-TYPE"));
  
    EXPECT_FALSE(IsNoCorsSafelistedHeaderName("range"));
    EXPECT_FALSE(IsNoCorsSafelistedHeaderName("cookie"));
    EXPECT_FALSE(IsNoCorsSafelistedHeaderName("foobar"));
  }
  
  TEST_F(CorsTest, PrivilegedNoCorsHeaderName) {
    EXPECT_TRUE(IsPrivilegedNoCorsHeaderName("range"));
    EXPECT_TRUE(IsPrivilegedNoCorsHeaderName("RanGe"));
    EXPECT_FALSE(IsPrivilegedNoCorsHeaderName("content-type"));
    EXPECT_FALSE(IsPrivilegedNoCorsHeaderName("foobar"));
    EXPECT_FALSE(IsPrivilegedNoCorsHeaderName("cookie"));
  }
  
  TEST_F(CorsTest, IsForbiddenMethod) {
    EXPECT_TRUE(IsForbiddenMethod("connect"));
    EXPECT_TRUE(IsForbiddenMethod("CONNECT"));
    EXPECT_TRUE(IsForbiddenMethod("Connect"));
    EXPECT_TRUE(IsForbiddenMethod("CoNnEcT"));
    EXPECT_FALSE(IsForbiddenMethod("C0NNECT"));
  
    EXPECT_TRUE(IsForbiddenMethod("trace"));
    EXPECT_TRUE(IsForbiddenMethod("track"));
    EXPECT_FALSE(IsForbiddenMethod("trac"));
    EXPECT_FALSE(IsForbiddenMethod("tracz"));
  }
  
  }  // namespace
  }  // namespace cors
  }  // namespace network
  