chromium/chrome/browser/ash/net/ash_proxy_monitor_browsertest.cc

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/net/ash_proxy_monitor.h"

#include <tuple>

#include "ash/constants/ash_pref_names.h"
#include "base/test/repeating_test_future.h"
#include "base/test/test_future.h"
#include "chrome/browser/ash/crosapi/prefs_ash.h"
#include "chrome/browser/ash/net/ash_proxy_monitor.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chromeos/ash/components/dbus/shill/shill_ipconfig_client.h"
#include "chromeos/ash/components/dbus/shill/shill_profile_client.h"
#include "chromeos/ash/components/dbus/shill/shill_service_client.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/crosapi/mojom/network_settings_service.mojom.h"
#include "chromeos/crosapi/mojom/prefs.mojom.h"
#include "components/policy/core/browser/browser_policy_connector.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/policy_types.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/pref_test_utils.h"
#include "components/proxy_config/proxy_config_dictionary.h"
#include "components/proxy_config/proxy_config_pref_names.h"
#include "components/proxy_config/proxy_prefs.h"
#include "content/public/test/browser_test.h"
#include "third_party/cros_system_api/dbus/shill/dbus-constants.h"

namespace {

constexpr char kPacUrl[] = "http://pac.pac/";

constexpr char kExtensionName[] = "Lacros Test Extension Name";
constexpr char kExtensionId[] = "Lacros Test Extension ID";
constexpr char kPrefExtensionNameKey[] = "extension_name_key";
constexpr char kPrefExtensionIdKey[] = "extension_id_key";
constexpr char kPrefExtensionCanDisabledKey[] = "can_be_disabled_key";

constexpr char kDefaultServicePath[] = "default_wifi";
constexpr char kDefaultServiceSsid[] = "default_wifi_guid";
constexpr char kDefaultServiceGuid[] = "eth0";

constexpr char kUserProfilePath[] = "user_profile";
constexpr char kWifi0ServicePath[] = "stub_wifi";
constexpr char kWifi0Ssid[] = "wifi0";
constexpr char kWifi0Guid[] = "{wifi0_guid}";

constexpr char kWifi1ServicePath[] = "stub_wifi1";
constexpr char kWifi1Ssid[] = "wifi1";
constexpr char kWifi1Guid[] = "{wifi1_guid}";

constexpr char kONCPolicyWifi0Proxy[] =
    R"({
     "NetworkConfigurations": [ {
        "GUID": "{wifi0_guid}",
        "Name": "wifi0",
        "ProxySettings": {
           "Manual": {
              "HTTPProxy": {
                 "Host": "http://proxy.com",
                 "Port": 3128
              }
           },
           "Type": "Manual"
        },
        "Type": "WiFi",
        "WiFi": {
           "AutoConnect": true,
           "HiddenSSID": false,
           "SSID": "wifi0",
           "Security": "None"
        }
     } ]
    })";

constexpr char kONCPolicyWifi1Pac[] =
    R"({
      "NetworkConfigurations": [ {
         "GUID": "{wifi1_guid}",
         "Name": "wifi1",
         "ProxySettings": {
            "Type": "PAC",
            "PAC": "http://pac.foo.com/script.pac"
         },
         "Type": "WiFi",
         "WiFi": {
            "AutoConnect": true,
            "HiddenSSID": false,
            "SSID": "wifi0",
            "Security": "None"
         }
      } ]
    })";
constexpr char kExpectedOncPacUrl[] = "http://pac.foo.com/script.pac";

base::Value::Dict GetPacProxyConfig(const std::string& pac_url) {
  return ProxyConfigDictionary::CreatePacScript(pac_url,
                                                /*pac_mandatory=*/false);
}

base::Value::Dict GetManualProxyConfig(const std::string& proxy_servers) {
  return ProxyConfigDictionary::CreateFixedServers(
      proxy_servers, /*bypass_list=*/std::string());
}

class TestPrefObserver : public crosapi::mojom::PrefObserver {
 public:
  TestPrefObserver() = default;
  TestPrefObserver(const TestPrefObserver&) = delete;
  TestPrefObserver& operator=(const TestPrefObserver&) = delete;
  ~TestPrefObserver() override {}

  // crosapi::mojom::PrefObserver:
  void OnPrefChanged(base::Value value) override {
    future_.AddValue(std::move(value));
  }
  base::Value Wait() { return future_.Take(); }
  base::test::RepeatingTestFuture<base::Value> future_;
  mojo::Receiver<crosapi::mojom::PrefObserver> receiver_{this};
};

}  // namespace

namespace ash {

class TestAshProxyMonitorObserver : public AshProxyMonitor::Observer {
 public:
  TestAshProxyMonitorObserver() {
    g_browser_process->platform_part()->ash_proxy_monitor()->AddObserver(this);
  }
  TestAshProxyMonitorObserver(const TestAshProxyMonitorObserver&) = delete;
  TestAshProxyMonitorObserver& operator=(const TestAshProxyMonitorObserver&) =
      delete;
  ~TestAshProxyMonitorObserver() override {
    g_browser_process->platform_part()->ash_proxy_monitor()->RemoveObserver(
        this);
  }

  std::tuple<base::Value::Dict, GURL> WaitForUpdate() { return future_.Take(); }

  bool AreAllProxyUpdatesRead() { return future_.IsEmpty(); }

 private:
  void OnProxyChanged() override {
    future_.AddValue(g_browser_process->platform_part()
                         ->ash_proxy_monitor()
                         ->GetLatestProxyConfig()
                         ->GetDictionary()
                         .Clone(),
                     g_browser_process->platform_part()
                         ->ash_proxy_monitor()
                         ->GetLatestWpadUrl());
  }

  base::test::RepeatingTestFuture<base::Value::Dict, GURL> future_;
};

class AshProxyMonitorTest : public InProcessBrowserTest {
 public:
  AshProxyMonitorTest() = default;
  AshProxyMonitorTest(const AshProxyMonitorTest&) = delete;
  AshProxyMonitorTest& operator=(const AshProxyMonitorTest&) = delete;
  ~AshProxyMonitorTest() override = default;

 protected:
  void SetUpInProcessBrowserTestFixture() override {
    InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
    ON_CALL(provider_, IsInitializationComplete(testing::_))
        .WillByDefault(testing::Return(true));
    ON_CALL(provider_, IsFirstPolicyLoadComplete(testing::_))
        .WillByDefault(testing::Return(true));
    policy::BrowserPolicyConnector::SetPolicyProviderForTesting(&provider_);
  }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    SetupFakePrefService();
    SetupNetworkEnvironment();
    ash_proxy_monitor_observer_ =
        std::make_unique<TestAshProxyMonitorObserver>();
    g_browser_process->platform_part()
        ->ash_proxy_monitor()
        ->SetProfileForTesting(browser()->profile());
  }

  void TearDownOnMainThread() override {
    ash_proxy_monitor_observer_.reset();
    prefs_observer_.reset();
  }

  void SetupNetworkEnvironment() {
    ash::ShillProfileClient::TestInterface* profile_test =
        ash::ShillProfileClient::Get()->GetTestInterface();
    ash::ShillServiceClient::TestInterface* service_test =
        ash::ShillServiceClient::Get()->GetTestInterface();
    profile_test->AddProfile(kUserProfilePath, "user");
    service_test->ClearServices();
    ConnectWifiNetworkService(kDefaultServicePath, kDefaultServiceSsid,
                              kDefaultServiceGuid);
  }

  void SetupFakePrefService() {
    prefs_ash_ = std::make_unique<crosapi::PrefsAsh>(
        g_browser_process->profile_manager(), g_browser_process->local_state());
    mojo::Remote<crosapi::mojom::Prefs> prefs_ash_remote;
    prefs_ash_->BindReceiver(prefs_ash_remote.BindNewPipeAndPassReceiver());
    prefs_observer_ = std::make_unique<TestPrefObserver>();
    prefs_ash_remote->AddObserver(
        crosapi::mojom::PrefPath::kProxy,
        prefs_observer_->receiver_.BindNewPipeAndPassRemote());
    prefs_ash_->OnProfileAdded(browser()->profile());
  }

  void ConnectWifiNetworkService(const std::string& service_path,
                                 const std::string& guid,
                                 const std::string& ssid) {
    ShillServiceClient::TestInterface* service_test =
        ShillServiceClient::Get()->GetTestInterface();

    service_test->AddService(service_path, guid, ssid, shill::kTypeWifi,
                             shill::kStateOnline, true /* add_to_visible */);

    service_test->SetServiceProperty(service_path, shill::kProfileProperty,
                                     base::Value(kUserProfilePath));
  }

  void DisconnectNetworkService(const std::string& service_path) {
    ShillServiceClient::TestInterface* service_test =
        ShillServiceClient::Get()->GetTestInterface();
    base::Value value(shill::kStateIdle);
    service_test->SetServiceProperty(service_path, shill::kStateProperty,
                                     value);
  }

  void ClearProxyPrefFromLacrosExtension() {
    base::test::TestFuture<void> future;
    prefs_ash_->ClearExtensionControlledPref(crosapi::mojom::PrefPath::kProxy,
                                             future.GetCallback());
    EXPECT_TRUE(future.Wait());
  }

  void SetProxyPrefFromLacrosExtension(base::Value::Dict proxy_dict) {
    base::test::TestFuture<void> future;
    prefs_ash_->SetPref(crosapi::mojom::PrefPath::kProxy,
                        base::Value(std::move(proxy_dict)),
                        future.GetCallback());
    EXPECT_TRUE(future.Wait());
  }

  void SetDhcpWpadUrl(const std::string& dhcp_url,
                      const std::string& service_path) {
    auto wpad_config = base::Value::Dict().Set(
        shill::kWebProxyAutoDiscoveryUrlProperty, base::Value(dhcp_url));
    const std::string kIPConfigPath = "test_ip_config";
    ash::ShillIPConfigClient::Get()->GetTestInterface()->AddIPConfig(
        kIPConfigPath, std::move(wpad_config));

    ash::ShillServiceClient::TestInterface* service_test =
        ash::ShillServiceClient::Get()->GetTestInterface();

    service_test->SetServiceProperty(service_path, shill::kIPConfigProperty,
                                     base::Value(kIPConfigPath));
  }
  std::unique_ptr<crosapi::PrefsAsh> prefs_ash_;
  std::unique_ptr<TestPrefObserver> prefs_observer_;

  policy::MockConfigurationPolicyProvider provider_;
  std::unique_ptr<TestAshProxyMonitorObserver> ash_proxy_monitor_observer_;
};

// Verifies that the `AshProxyMonitor` listens to default network
// changes and propagates the proxy configuration to observers.
IN_PROC_BROWSER_TEST_F(AshProxyMonitorTest, DefaultNetworkChanges) {
  // Verify that observers get updated when connecting to a new network with a
  // proxy configured.
  DisconnectNetworkService(kDefaultServicePath);
  ConnectWifiNetworkService(kWifi0ServicePath, kWifi0Guid, kWifi0Ssid);
  policy::PolicyMap policy;
  policy.Set(policy::key::kOpenNetworkConfiguration,
             policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
             policy::POLICY_SOURCE_CLOUD, base::Value(kONCPolicyWifi0Proxy),
             nullptr);
  provider_.UpdateChromePolicy(policy);

  constexpr char kEmptyWpadUrl[] = "";
  std::tuple<base::Value::Dict, GURL> result =
      ash_proxy_monitor_observer_->WaitForUpdate();
  EXPECT_EQ(std::get<0>(result), GetManualProxyConfig("http=proxy.com:3128"));
  EXPECT_EQ(std::get<1>(result), kEmptyWpadUrl);

  // Verify that observers get updated when the default network receives a WPAD
  // URL via DHCP.
  constexpr char kWebProxyAutodetectionUrl[] = "www.proxyurl.com:443";
  SetDhcpWpadUrl(kWebProxyAutodetectionUrl, kWifi0ServicePath);

  result = ash_proxy_monitor_observer_->WaitForUpdate();
  EXPECT_EQ(std::get<0>(result), GetManualProxyConfig("http=proxy.com:3128"));
  EXPECT_EQ(std::get<1>(result), kWebProxyAutodetectionUrl);

  // Verify that observers get updated when switching networks with a different
  // proxy config and no WPAD URL.
  DisconnectNetworkService(kWifi0ServicePath);
  ConnectWifiNetworkService(kWifi1ServicePath, kWifi1Guid, kWifi1Ssid);
  policy.Set(policy::key::kOpenNetworkConfiguration,
             policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
             policy::POLICY_SOURCE_CLOUD, base::Value(kONCPolicyWifi1Pac),
             nullptr);
  provider_.UpdateChromePolicy(policy);

  result = ash_proxy_monitor_observer_->WaitForUpdate();
  EXPECT_EQ(std::get<0>(result), GetPacProxyConfig(kExpectedOncPacUrl));
  EXPECT_EQ(std::get<1>(result), kEmptyWpadUrl);

  EXPECT_TRUE(ash_proxy_monitor_observer_->AreAllProxyUpdatesRead());
}

// Verifies that the `AshProxyMonitor` listens to kProxy pref changes and
// propagates the proxy configuration to observers.
IN_PROC_BROWSER_TEST_F(AshProxyMonitorTest, ProxyPrefChanges) {
  // Set the pref via policy.
  policy::PolicyMap policy;
  policy.Set(policy::key::kProxyMode, policy::POLICY_LEVEL_MANDATORY,
             policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_CLOUD,
             base::Value(ProxyPrefs::kPacScriptProxyModeName), nullptr);
  policy.Set(policy::key::kProxyPacUrl, policy::POLICY_LEVEL_MANDATORY,
             policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_CLOUD,
             base::Value(kPacUrl), nullptr);
  provider_.UpdateChromePolicy(policy);

  std::tuple<base::Value::Dict, GURL> result =
      ash_proxy_monitor_observer_->WaitForUpdate();
  EXPECT_EQ(std::get<0>(result), GetPacProxyConfig(kPacUrl));
  EXPECT_FALSE(g_browser_process->platform_part()
                   ->ash_proxy_monitor()
                   ->IsLacrosExtensionControllingProxy());

  // Clear the policy pref.
  policy.Erase(policy::key::kProxyMode);
  policy.Erase(policy::key::kProxyPacUrl);
  provider_.UpdateChromePolicy(policy);
  result = ash_proxy_monitor_observer_->WaitForUpdate();
  EXPECT_EQ(std::get<0>(result), ProxyConfigDictionary::CreateDirect());

  EXPECT_TRUE(ash_proxy_monitor_observer_->AreAllProxyUpdatesRead());
}

IN_PROC_BROWSER_TEST_F(AshProxyMonitorTest, OrderOfPrecedence) {
  DisconnectNetworkService(kDefaultServicePath);
  ConnectWifiNetworkService(kWifi0ServicePath, kWifi0Guid, kWifi0Ssid);

  policy::PolicyMap policy;
  policy.Set(policy::key::kProxyMode, policy::POLICY_LEVEL_MANDATORY,
             policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_CLOUD,
             base::Value(ProxyPrefs::kPacScriptProxyModeName), nullptr);
  policy.Set(policy::key::kProxyPacUrl, policy::POLICY_LEVEL_MANDATORY,
             policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_CLOUD,
             base::Value(kPacUrl), nullptr);
  policy.Set(policy::key::kOpenNetworkConfiguration,
             policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
             policy::POLICY_SOURCE_CLOUD, base::Value(kONCPolicyWifi0Proxy),
             nullptr);
  provider_.UpdateChromePolicy(policy);
  std::tuple<base::Value::Dict, GURL> result =
      ash_proxy_monitor_observer_->WaitForUpdate();
  EXPECT_EQ(std::get<0>(result), GetPacProxyConfig(kPacUrl));

  // Clear the global proxy.
  policy.Erase(policy::key::kProxyMode);
  policy.Erase(policy::key::kProxyPacUrl);
  provider_.UpdateChromePolicy(policy);

  // The proxy configured on the local network is active.
  result = ash_proxy_monitor_observer_->WaitForUpdate();
  EXPECT_EQ(std::get<0>(result), GetManualProxyConfig("http=proxy.com:3128"));

  EXPECT_TRUE(ash_proxy_monitor_observer_->AreAllProxyUpdatesRead());
}

IN_PROC_BROWSER_TEST_F(AshProxyMonitorTest, LacrosExtensionProxyPrefChanges) {
  SetProxyPrefFromLacrosExtension(GetPacProxyConfig(kPacUrl));
  std::tuple<base::Value::Dict, GURL> result =
      ash_proxy_monitor_observer_->WaitForUpdate();
  EXPECT_EQ(std::get<0>(result), GetPacProxyConfig(kPacUrl));

  auto* ash_proxy_monitor =
      g_browser_process->platform_part()->ash_proxy_monitor();

  EXPECT_TRUE(ash_proxy_monitor->IsLacrosExtensionControllingProxy());

  // Update the extension metadata.
  ash_proxy_monitor->SetLacrosExtensionControllingProxyInfo(
      kExtensionName, kExtensionId, false);

  auto* pref_service = browser()->profile()->GetPrefs();
  EXPECT_EQ(pref_service->GetDict(ash::prefs::kLacrosProxyControllingExtension),
            base::Value::Dict()
                .Set(kPrefExtensionNameKey, kExtensionName)
                .Set(kPrefExtensionIdKey, kExtensionId)
                .Set(kPrefExtensionCanDisabledKey, false));
  auto extension = ash_proxy_monitor->GetLacrosExtensionControllingTheProxy();
  EXPECT_EQ(extension->name, kExtensionName);
  EXPECT_EQ(extension->id, kExtensionId);
  EXPECT_EQ(extension->can_be_disabled, false);
  ClearProxyPrefFromLacrosExtension();
  ash_proxy_monitor->ClearLacrosExtensionControllingProxyInfo();

  while (std::get<0>(result) != ProxyConfigDictionary::CreateDirect()) {
    result = ash_proxy_monitor_observer_->WaitForUpdate();
  }

  EXPECT_FALSE(ash_proxy_monitor->GetLacrosExtensionControllingTheProxy());
}

}  // namespace ash