// Copyright 2013 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 "components/password_manager/core/browser/psl_matching_helper.h"

#include <stddef.h>
#include <cctype>

#include "base/macros.h"
#include "base/stl_util.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "components/autofill/core/common/password_form.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace password_manager {

namespace {

TEST(PSLMatchingUtilsTest, GetMatchResultNormalCredentials) {
  struct TestData {
    const char* form_origin;
    const char* digest_origin;
    MatchResult match_result;
  };

  const TestData cases[] = {
      // Test Exact Matches.
      {"http://facebook.com/p", "http://facebook.com/p1",
       MatchResult::EXACT_MATCH},

      {"http://m.facebook.com/p", "http://m.facebook.com/p1",
       MatchResult::EXACT_MATCH},

      // Scheme mismatch.
      {"http://www.example.com", "https://www.example.com",
       MatchResult::NO_MATCH},

      // Host mismatch.
      {"http://www.example.com", "http://wwwexample.com",
       MatchResult::NO_MATCH},

      {"http://www.example1.com", "http://www.example2.com",
       MatchResult::NO_MATCH},

      // Port mismatch.
      {"http://www.example.com:123/", "http://www.example.com",
       MatchResult::NO_MATCH},

      // TLD mismatch.
      {"http://www.example.org/", "http://www.example.com/",
       MatchResult::NO_MATCH},

      // URLs without registry controlled domains should not match.
      {"http://localhost/", "http://127.0.0.1/", MatchResult::NO_MATCH},

      // Invalid URLs don't match.
      {"http://www.example.com/", "http://", MatchResult::NO_MATCH},

      {"", "http://www.example.com/", MatchResult::NO_MATCH},

      {"http://www.example.com", "bad url", MatchResult::NO_MATCH}};

  for (const TestData& data : cases) {
    autofill::PasswordForm form;
    form.origin = GURL(data.form_origin);
    form.signon_realm = form.origin.GetOrigin().spec();
    PasswordStore::FormDigest digest(
        autofill::PasswordForm::Scheme::kHtml,
        GURL(data.digest_origin).GetOrigin().spec(), GURL(data.digest_origin));

    EXPECT_EQ(data.match_result, GetMatchResult(form, digest))
        << "form_origin = " << data.form_origin << ", digest = " << digest;
  }
}

TEST(PSLMatchingUtilsTest, GetMatchResultPSL) {
  struct TestData {
    const char* form_origin;
    const char* digest_origin;
    MatchResult match_result;
  };

  const TestData cases[] = {
      // Test PSL Matches.
      {"http://facebook.com/p1", "http://m.facebook.com/p2",
       MatchResult::PSL_MATCH},

      {"https://www.facebook.com/", "https://m.facebook.com",
       MatchResult::PSL_MATCH},

      // Don't apply PSL matching to Google domains.
      {"https://google.com/", "https://maps.google.com/",
       MatchResult::NO_MATCH},

      // Scheme mismatch.
      {"http://facebook.com/", "https://m.facebook.com/",
       MatchResult::NO_MATCH},

      {"https://facebook.com/", "http://m.facebook.com/",
       MatchResult::NO_MATCH},

      // Port mismatch.
      {"http://facebook.com/", "https://m.facebook.com:8080/",
       MatchResult::NO_MATCH},

      {"http://facebook.com:8080/", "https://m.facebook.com/",
       MatchResult::NO_MATCH},

      // Port match.
      {"http://facebook.com:8080/p1", "http://m.facebook.com:8080/p2",
       MatchResult::PSL_MATCH},
  };

  for (const TestData& data : cases) {
    autofill::PasswordForm form;
    form.origin = GURL(data.form_origin);
    form.signon_realm = form.origin.GetOrigin().spec();
    PasswordStore::FormDigest digest(
        autofill::PasswordForm::Scheme::kHtml,
        GURL(data.digest_origin).GetOrigin().spec(), GURL(data.digest_origin));

    EXPECT_EQ(data.match_result, GetMatchResult(form, digest))
        << "form_origin = " << data.form_origin << ", digest = " << digest;
  }
}

TEST(PSLMatchingUtilsTest, GetMatchResultFederated) {
  struct TestData {
    const char* form_origin;
    const char* form_federation_origin;
    const char* digest_origin;
    MatchResult match_result;
  };

  const TestData cases[] = {
      // Test Federated Matches.
      {"https://example.com/p", "https://google.com", "https://example.com/p2",
       MatchResult::FEDERATED_MATCH},

      // Empty federation providers don't match.
      {"https://example.com/", "", "https://example.com",
       MatchResult::NO_MATCH},

      // Invalid origins don't match.
      {"https://example.com/", "https://google.com", "example.com",
       MatchResult::NO_MATCH},

      {"https://example.com/", "https://google.com", "example",
       MatchResult::NO_MATCH},

      // TLD Mismatch.
      {"https://example.com/", "https://google.com", "https://example.org",
       MatchResult::NO_MATCH},

      // Scheme mismatch.
      {"http://example.com/", "https://google.com", "https://example.com/",
       MatchResult::NO_MATCH},

      {"https://example.com/", "https://google.com", "http://example.com/",
       MatchResult::NO_MATCH},

      // Port mismatch.
      {"https://localhost/", "https://google.com", "http://localhost:8080",
       MatchResult::NO_MATCH},

      {"https://localhost:8080", "https://google.com", "http://localhost",
       MatchResult::NO_MATCH},

      // Port match.
      {"https://localhost:8080/p", "https://google.com",
       "https://localhost:8080/p2", MatchResult::FEDERATED_MATCH},
  };

  for (const TestData& data : cases) {
    autofill::PasswordForm form;
    form.origin = GURL(data.form_origin);
    form.federation_origin =
        url::Origin::Create(GURL(data.form_federation_origin));
    form.signon_realm = "federation://" + form.origin.host() + "/" +
                        form.federation_origin.host();

    PasswordStore::FormDigest digest(
        autofill::PasswordForm::Scheme::kHtml,
        GURL(data.digest_origin).GetOrigin().spec(), GURL(data.digest_origin));

    EXPECT_EQ(data.match_result, GetMatchResult(form, digest))
        << "form_origin = " << data.form_origin
        << ", form_federation_origin = " << data.form_federation_origin
        << ", digest = " << digest;
  }
}

TEST(PSLMatchingUtilsTest, GetMatchResultFederatedPSL) {
  struct TestData {
    const char* form_origin;
    const char* form_federation_origin;
    const char* digest_origin;
    MatchResult match_result;
  };

  const TestData cases[] = {
      // Test Federated PSL Matches.
      {"https://sub.example.com/p2", "https://google.com",
       "https://sub.example.com/p1", MatchResult::FEDERATED_MATCH},

      {"https://sub1.example.com/p1", "https://google.com",
       "https://sub2.example.com/p2", MatchResult::FEDERATED_PSL_MATCH},

      {"https://example.com/", "https://google.com", "https://sub.example.com",
       MatchResult::FEDERATED_PSL_MATCH},

      // Federated PSL matches do not apply to HTTP.
      {"https://sub1.example.com/", "https://google.com",
       "http://sub2.example.com", MatchResult::NO_MATCH},

      {"https://example.com/", "https://google.com", "http://sub.example.com",
       MatchResult::NO_MATCH},

      {"http://example.com/", "https://google.com", "https://example.com/",
       MatchResult::NO_MATCH},

      // Federated PSL matches do not apply to Google on HTTP or HTTPS.
      {"https://accounts.google.com/", "https://facebook.com",
       "https://maps.google.com", MatchResult::NO_MATCH},

      {"https://accounts.google.com/facebook.com", "https://facebook.com",
       "http://maps.google.com", MatchResult::NO_MATCH},

      // TLD Mismatch.
      {"https://sub.example.com/google.com", "https://google.com",
       "https://sub.example.org", MatchResult::NO_MATCH},

      // Port mismatch.
      {"https://example.com/", "https://google.com", "https://example.com:8080",
       MatchResult::NO_MATCH},

      {"https://example.com:8080", "https://google.com", "https://example.com",
       MatchResult::NO_MATCH},

      // Port match.
      {"https://sub.example.com:8080/p", "https://google.com",
       "https://sub2.example.com:8080/p2", MatchResult::FEDERATED_PSL_MATCH},
  };

  for (const TestData& data : cases) {
    autofill::PasswordForm form;
    form.origin = GURL(data.form_origin);
    form.federation_origin =
        url::Origin::Create(GURL(data.form_federation_origin));
    form.signon_realm = "federation://" + form.origin.host() + "/" +
                        form.federation_origin.host();

    PasswordStore::FormDigest digest(
        autofill::PasswordForm::Scheme::kHtml,
        GURL(data.digest_origin).GetOrigin().spec(), GURL(data.digest_origin));

    EXPECT_EQ(data.match_result, GetMatchResult(form, digest))
        << "form_origin = " << data.form_origin
        << ", form_federation_origin = " << data.form_federation_origin
        << ", digest = " << digest;
  }
}

TEST(PSLMatchingUtilsTest, IsPublicSuffixDomainMatch) {
  struct TestPair {
    const char* url1;
    const char* url2;
    bool should_match;
  };

  const TestPair pairs[] = {
      {"http://facebook.com", "http://facebook.com", true},
      {"http://facebook.com/path", "http://facebook.com/path", true},
      {"http://facebook.com/path1", "http://facebook.com/path2", true},
      {"http://facebook.com", "http://m.facebook.com", true},
      {"http://www.facebook.com", "http://m.facebook.com", true},
      {"http://facebook.com/path", "http://m.facebook.com/path", true},
      {"http://facebook.com/path1", "http://m.facebook.com/path2", true},
      {"http://example.com/has space", "http://example.com/has space", true},
      {"http://www.example.com", "http://wwwexample.com", false},
      {"http://www.example.com", "https://www.example.com", false},
      {"http://www.example.com:123", "http://www.example.com", false},
      {"http://www.example.org", "http://www.example.com", false},
      // URLs without registry controlled domains should not match.
      {"http://localhost", "http://127.0.0.1", false},
      // Invalid URLs should not match anything.
      {"http://", "http://", false},
      {"", "", false},
      {"bad url", "bad url", false},
      {"http://www.example.com", "http://", false},
      {"", "http://www.example.com", false},
      {"http://www.example.com", "bad url", false},
      {"http://www.example.com/%00", "http://www.example.com/%00", false},
      {"federation://example.com/google.com", "https://example.com/", false},
  };

  for (const TestPair& pair : pairs) {
    autofill::PasswordForm form1;
    form1.signon_realm = pair.url1;
    autofill::PasswordForm form2;
    form2.signon_realm = pair.url2;
    EXPECT_EQ(pair.should_match,
              IsPublicSuffixDomainMatch(form1.signon_realm, form2.signon_realm))
        << "First URL = " << pair.url1 << ", second URL = " << pair.url2;
  }
}

TEST(PSLMatchingUtilsTest, IsFederatedRealm) {
  struct TestPair {
    const char* form_signon_realm;
    const char* origin;
    bool should_match;
  };

  const TestPair pairs[] = {
      {"https://facebook.com", "https://facebook.com", false},
      {"", "", false},
      {"", "https://facebook.com/", false},
      {"https://facebook.com/", "", false},
      {"federation://example.com/google.com", "https://example.com/", true},
      {"federation://example.com/google.com", "http://example.com/", true},
      {"federation://example.com/google.com", "example.com", false},
      {"federation://example.com/", "http://example.com/", false},
      {"federation://example.com/google.com", "example", false},

      {"federation://localhost/google.com", "http://localhost/", true},
      {"federation://localhost/google.com", "http://localhost:8000/", true},
  };

  for (const TestPair& pair : pairs) {
    EXPECT_EQ(pair.should_match,
              IsFederatedRealm(pair.form_signon_realm, GURL(pair.origin)))
        << "form_signon_realm = " << pair.form_signon_realm
        << ", origin = " << pair.origin;
  }
}

TEST(PSLMatchingUtilsTest, IsFederatedPSLMatch) {
  struct TestPair {
    const char* form_signon_realm;
    const char* form_origin;
    const char* origin;
    bool should_match;
  };

  const TestPair pairs[] = {
      {"https://facebook.com", "https://facebook.com", "https://facebook.com",
       false},
      {"", "", "", false},
      {"", "", "https://facebook.com/", false},
      {"https://facebook.com/", "https://facebook.com/", "", false},

      {"federation://example.com/google.com", "https://example.com/p1",
       "https://example.com/p2", true},
      {"federation://example.com/google.com", "https://example.com/",
       "http://example.com/", false},
      {"federation://example.com/google.com", "https://example.com/",
       "example.com", false},
      {"federation://example.com/", "https://example.com/",
       "https://example.com/", false},
      {"federation://example.com/google.com", "https://example.com/", "example",
       false},

      {"federation://sub.example.com/google.com", "https://sub.example.com/p1",
       "https://sub.example.com/p2", true},
      {"federation://sub.example.com/google.com", "https://sub.example.com/p1",
       "https://sub2.example.com/p2", true},
      {"federation://example.com/google.com", "https://example.com/p1",
       "https://sub.example.com/", true},
      {"federation://example.com/google.com", "https://example.com/",
       "http://sub.example.com/", false},
      {"federation://sub.example.com/", "https://sub.example.com/",
       "https://sub.example.com/", false},

      {"federation://localhost/google.com", "http://localhost/",
       "http://localhost/", true},
      {"federation://localhost/google.com", "http://localhost:8000/",
       "http://localhost:8000/", true},
      {"federation://localhost/google.com", "http://localhost:8000/",
       "http://localhost/", false},
      {"federation://localhost/google.com", "http://localhost/",
       "http://localhost:8000/", false},
  };

  for (const TestPair& pair : pairs) {
    EXPECT_EQ(pair.should_match,
              IsFederatedPSLMatch(pair.form_signon_realm,
                                  GURL(pair.form_origin), GURL(pair.origin)))
        << "form_signon_realm = " << pair.form_signon_realm
        << "form_origin = " << pair.form_origin << ", origin = " << pair.origin;
  }
}

TEST(PSLMatchingUtilsTest, GetOrganizationIdentifyingName) {
  static constexpr const struct {
    const char* url;
    const char* expected_organization_name;
  } kTestCases[] = {
      {"http://example.com/login", "example"},
      {"https://example.com", "example"},
      {"ftp://example.com/ftp_realm", "example"},

      {"http://foo.bar.example.com", "example"},
      {"http://example.co.uk", "example"},
      {"http://bar.example.appspot.com", "example"},
      {"http://foo.bar", "foo"},
      {"https://user:pass@www.example.com:80/path?query#ref", "example"},

      {"http://www.foo+bar.com", "foo+bar"},
      {"http://www.foo-bar.com", "foo-bar"},
      {"https://foo_bar.com", "foo_bar"},
      {"http://www.foo%2Bbar.com", "foo+bar"},
      {"http://www.foo%2Dbar.com", "foo-bar"},
      {"https://foo%5Fbar.com", "foo_bar"},
      {"http://www.foo%2Ebar.com", "bar"},

      // Internationalized Domain Names: each dot-separated label of the domain
      // name is individually Punycode-encoded, so the organization-identifying
      // name is still well-defined and can be determined as normal.
      //       , ,
      //     szotar = sz\xc3\xb3t\xc3\xa1r (UTF-8) = xn--sztr-7na0i (IDN)
      //       | |
      //  U+00E1 U+00F3
      {"https://www.sz\xc3\xb3t\xc3\xa1r.appspot.com", "xn--sztr-7na0i"},

      {"http://www.foo!bar.com", "foo%21bar"},
      {"http://www.foo%21bar.com", "foo%21bar"},
      {"http://www.foo$bar.com", "foo%24bar"},
      {"http://www.foo&bar.com", "foo%26bar"},
      {"http://www.foo\'bar.com", "foo%27bar"},
      {"http://www.foo(bar.com", "foo%28bar"},
      {"http://www.foo)bar.com", "foo%29bar"},
      {"http://www.foo*bar.com", "foo%2Abar"},
      {"http://www.foo,bar.com", "foo%2Cbar"},
      {"http://www.foo=bar.com", "foo%3Dbar"},

      // URLs without host portions, hosts without registry controlled domains
      // should, or hosts consisting of a registry yield the empty string.
      {"http://localhost", ""},
      {"http://co.uk", ""},
      {"http://google", ""},
      {"http://127.0.0.1", ""},
      {"file:///usr/bin/stuff", ""},
      {"federation://example.com/google.com", ""},
      {"android://hash@com.example/", ""},
      {"http://[1080:0:0:0:8:800:200C:417A]/", ""},
      {"http://[3ffe:2a00:100:7031::1]", ""},
      {"http://[::192.9.5.5]/", ""},

      // Invalid URLs should yield the empty string.
      {"", ""},
      {"http://", ""},
      {"bad url", ""},
      {"http://www.example.com/%00", ""},
      {"http://www.foo;bar.com", ""},
      {"http://www.foo~bar.com", ""},
  };

  for (const auto& test_case : kTestCases) {
    SCOPED_TRACE(test_case.url);
    GURL url(test_case.url);
    EXPECT_EQ(test_case.expected_organization_name,
              GetOrganizationIdentifyingName(url));
  }
}

// Apart from alphanumeric characters and '.', only |kExpectedUnescapedChars|
// are expected to appear without percent-encoding in the domain of a valid,
// canonicalized URL.
//
// The purpose of this test is to ensure that the test cases around unescaped
// special characters in `GetOrganizationIdentifyingName` are exhaustive.
TEST(PSLMatchingUtilsTest,
     GetOrganizationIdentifyingName_UnescapedSpecialChars) {
  static constexpr const char kExpectedNonAlnumChars[] = {'+', '-', '_', '.'};
  for (int chr = 0; chr <= 255; ++chr) {
    const auto percent_encoded = base::StringPrintf("http://a%%%02Xb.hu/", chr);
    const GURL url(percent_encoded);
    if (isalnum(chr) || base::Contains(kExpectedNonAlnumChars, chr)) {
      ASSERT_TRUE(url.is_valid());
      const auto percent_decoded = base::StringPrintf(
          "http://a%cb.hu/", base::ToLowerASCII(static_cast<char>(chr)));
      EXPECT_EQ(percent_decoded, url.spec());
    } else if (url.is_valid()) {
      EXPECT_EQ(percent_encoded, url.spec());
    }
  }
}

}  // namespace

}  // namespace password_manager
