chromium/chrome/browser/lacros/net/proxy_config_service_lacros_browsertest.cc

// Copyright 2021 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/lacros/net/proxy_config_service_lacros.h"

#include "base/time/time.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/lacros/net/network_settings_translation.h"
#include "chrome/browser/net/proxy_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chromeos/crosapi/mojom/network_settings_service.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "components/prefs/pref_service.h"
#include "components/proxy_config/pref_proxy_config_tracker_impl.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 "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote_set.h"
#include "net/proxy_resolution/proxy_config_service.h"

namespace {

constexpr char kPacUrl[] = "pac.pac";

// Fakes the NetworkSettingsService in Ash-Chrome so we can send proxy updates
// from the tests via the mojo API.
class FakeNetworkSettingsService
    : public crosapi::mojom::NetworkSettingsService {
 public:
  FakeNetworkSettingsService() = default;
  FakeNetworkSettingsService(const FakeNetworkSettingsService&) = delete;
  FakeNetworkSettingsService& operator=(const FakeNetworkSettingsService&) =
      delete;
  ~FakeNetworkSettingsService() override = default;

  bool HasObservers() { return !observers_.empty(); }

  // crosapi::mojom::AshNetworService:
  void AddNetworkSettingsObserver(
      mojo::PendingRemote<crosapi::mojom::NetworkSettingsObserver> observer)
      override {
    mojo::Remote<crosapi::mojom::NetworkSettingsObserver> remote(
        std::move(observer));
    if (cached_proxy_config_) {
      remote->OnProxyChanged(cached_proxy_config_.Clone());
    }
    observers_.Add(std::move(remote));

    if (quit_closure_) {
      std::move(quit_closure_).Run();
    }
  }

  void IsAlwaysOnVpnPreConnectUrlAllowlistEnforced(
      IsAlwaysOnVpnPreConnectUrlAllowlistEnforcedCallback callback) override {}

  // This fake implementation of the crosapi::mojom::NetworkSettingsService is
  // only used to test the behaviour of `ProxyConfigServiceLacros`, which is an
  // observer of the mojo service. Observers only listen for updates, they do
  // not send data to the service. Extension set proxy are tested by the test
  // suite LacrosExtensionProxyTrackerTest whose fixture supports installing
  // extension.
  void SetExtensionProxy(crosapi::mojom::ProxyConfigPtr proxy_config) override {
    NOTREACHED();
  }
  void ClearExtensionProxy() override { NOTREACHED(); }
  void SetExtensionControllingProxyMetadata(
      crosapi::mojom::ExtensionControllingProxyPtr extension) override {
    NOTREACHED();
  }
  void ClearExtensionControllingProxyMetadata() override { NOTREACHED(); }

  void SetQuitClosure(base::OnceClosure quit_closure) {
    quit_closure_ = std::move(quit_closure);
  }

  void SendProxyUpdate(crosapi::mojom::ProxyConfigPtr proxy_config) {
    cached_proxy_config_ = std::move(proxy_config);
    for (const auto& obs : observers_) {
      obs->OnProxyChanged(cached_proxy_config_.Clone());
    }
  }

 private:
  crosapi::mojom::ProxyConfigPtr cached_proxy_config_;
  mojo::RemoteSet<crosapi::mojom::NetworkSettingsObserver> observers_;
  base::OnceClosure quit_closure_;
};

// Fakes the ProxyConfigMonitor which is the class that sends the proxy updates
// to the Browser's NetworkService process via mojo.
class FakeProxyMonitor : public net::ProxyConfigService::Observer {
 public:
  FakeProxyMonitor() = default;
  FakeProxyMonitor(const FakeProxyMonitor&) = delete;
  FakeProxyMonitor& operator=(const FakeProxyMonitor&) = delete;
  ~FakeProxyMonitor() override = default;

  void OnProxyConfigChanged(
      const net::ProxyConfigWithAnnotation& config,
      net::ProxyConfigService::ConfigAvailability availability) override {
    cached_proxy_config_ = std::move(config);
    if (quit_closure_) {
      std::move(quit_closure_).Run();
    }
  }

  void SetQuitClosure(base::OnceClosure quit_closure) {
    quit_closure_ = std::move(quit_closure);
  }
  net::ProxyConfigWithAnnotation cached_proxy_config_;

 private:
  base::OnceClosure quit_closure_;
};

}  // namespace

namespace chromeos {

class ProxyConfigServiceLacrosTest : public InProcessBrowserTest {
 protected:
  ProxyConfigServiceLacrosTest() = default;

  ProxyConfigServiceLacrosTest(const ProxyConfigServiceLacrosTest&) = delete;
  ProxyConfigServiceLacrosTest& operator=(const ProxyConfigServiceLacrosTest&) =
      delete;
  ~ProxyConfigServiceLacrosTest() override = default;
  mojo::Remote<crosapi::mojom::NetworkSettingsService>
      network_settings_service_;

  bool IsServiceAvailable() {
    auto* lacros_service = chromeos::LacrosService::Get();
    if (!lacros_service)
      return false;
    return lacros_service
        ->IsAvailable<crosapi::mojom::NetworkSettingsService>();
  }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    // If the lacros service or the network settings service interface are not
    // available on this version of ash-chrome, this test suite will no-op.
    if (!IsServiceAvailable())
      return;
    // Replace the production network settings service with a fake for testing.
    mojo::Remote<crosapi::mojom::NetworkSettingsService>& remote =
        chromeos::LacrosService::Get()
            ->GetRemote<crosapi::mojom::NetworkSettingsService>();
    remote.reset();
    receiver_.Bind(remote.BindNewPipeAndPassReceiver());
    SetupProxyMonitoring(browser()->profile());
  }

  void TearDownOnMainThread() override { ResetProxyMonitoring(); }

 protected:
  // Sends a proxy update via the AshNetworkSettingService in Ash-Chrome and
  // waits for `proxy_monitor_` in Lacros-Chrome to receive the config.
  void SendAshProxyUpdateAndWait(crosapi::mojom::ProxyConfigPtr proxy_config) {
    base::RunLoop run_loop;
    proxy_monitor_->SetQuitClosure(run_loop.QuitClosure());
    service_.SendProxyUpdate(std::move(proxy_config));
    run_loop.Run();
  }

  // Creates the objects that monitor proxy configs coming from Ash and from
  // profile prefs.
  void SetupProxyMonitoring(Profile* profile) {
    pref_proxy_config_tracker_ =
        ProxyServiceFactory::CreatePrefProxyConfigTrackerOfProfile(
            profile->GetPrefs(), nullptr);

    base::RunLoop run_loop;
    service_.SetQuitClosure(run_loop.QuitClosure());
    proxy_config_service_ = ProxyServiceFactory::CreateProxyConfigService(
        pref_proxy_config_tracker_.get(), profile);
    // Wait for the mojom::NetworkSettingsObserver created by
    // `proxy_config_service_` in Lacros to be added as an observer to the
    // AshNetworkSettingsService in Ash-Chrome.
    run_loop.Run();

    proxy_monitor_ = std::make_unique<FakeProxyMonitor>();
    proxy_config_service_->AddObserver(proxy_monitor_.get());
  }

  void ResetProxyMonitoring() {
    proxy_config_service_->RemoveObserver(proxy_monitor_.get());
    pref_proxy_config_tracker_->DetachFromPrefService();
    pref_proxy_config_tracker_.reset();
    proxy_config_service_.reset();
    proxy_monitor_.reset();
    base::RunLoop().RunUntilIdle();
  }

  Profile& CreateSecondaryProfile() {
    ProfileManager* profile_manager = g_browser_process->profile_manager();
    base::FilePath profile_path =
        profile_manager->GenerateNextProfileDirectoryPath();
    return profiles::testing::CreateProfileSync(profile_manager, profile_path);
  }

  std::unique_ptr<net::ProxyConfigService> proxy_config_service_;
  // Monitors global and profile prefs related to proxy configuration.
  std::unique_ptr<PrefProxyConfigTracker> pref_proxy_config_tracker_;
  // Fakes the ProxyConfigMonitor which directly updates the Browser's
  // NetworkService process.
  std::unique_ptr<FakeProxyMonitor> proxy_monitor_;
  FakeNetworkSettingsService service_;
  mojo::Receiver<crosapi::mojom::NetworkSettingsService> receiver_{&service_};
};

// Tests that the chromeos::ProxyConfigServiceLacros instance internally created
// by `proxy_config_service_` is added to Ash-Chrome as an observer for network
// changes.
IN_PROC_BROWSER_TEST_F(ProxyConfigServiceLacrosTest, ObserverSet) {
  if (!IsServiceAvailable())
    return;
  ASSERT_TRUE(service_.HasObservers());
}

// Tests that proxy updates from the AshNetworkService in Ash-Chrome are
// correctly propagated to observers of the `proxy_config_service_` in
// Lacros-Chrome.
IN_PROC_BROWSER_TEST_F(ProxyConfigServiceLacrosTest, ProxyUpdates) {
  if (!IsServiceAvailable())
    return;

  crosapi::mojom::ProxyConfigPtr proxy_config =
      crosapi::mojom::ProxyConfig::New();

  proxy_config->proxy_settings = crosapi::mojom::ProxySettings::NewDirect(
      crosapi::mojom::ProxySettingsDirect::New());
  SendAshProxyUpdateAndWait(proxy_config.Clone());
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            net::ProxyConfig::CreateDirect().ToValue());

  crosapi::mojom::ProxySettingsWpadPtr wpad =
      crosapi::mojom::ProxySettingsWpad::New();
  wpad->pac_url = GURL(kPacUrl);
  // TODO(crbug.com/40223591): This test seems buggy; wpad is never used.
  proxy_config->proxy_settings = crosapi::mojom::ProxySettings::NewWpad(
      crosapi::mojom::ProxySettingsWpad::New());
  SendAshProxyUpdateAndWait(proxy_config.Clone());
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            CrosapiProxyToNetProxy(proxy_config.Clone()).value().ToValue());

  crosapi::mojom::ProxySettingsManualPtr manual =
      crosapi::mojom::ProxySettingsManual::New();
  crosapi::mojom::ProxyLocationPtr location =
      crosapi::mojom::ProxyLocation::New();
  location->host = "proxy";
  location->port = 80;
  manual->secure_http_proxies.push_back(location.Clone());
  manual->socks_proxies.push_back(location.Clone());
  proxy_config->proxy_settings->set_manual(std::move(manual));
  SendAshProxyUpdateAndWait(proxy_config.Clone());
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            CrosapiProxyToNetProxy(std::move(proxy_config)).value().ToValue());
}

// Tests that proxies set via the user pref `kProxy` have precedence over system
// proxies coming from Ash-Chrome.
IN_PROC_BROWSER_TEST_F(ProxyConfigServiceLacrosTest, UserPrefPrecedence) {
  if (!IsServiceAvailable())
    return;
  // Set a proxy via pref.
  base::RunLoop run_loop;
  proxy_monitor_->SetQuitClosure(run_loop.QuitClosure());
  base::Value::Dict proxy_config_wpad;
  proxy_config_wpad.Set("mode", ProxyPrefs::kAutoDetectProxyModeName);
  browser()->profile()->GetPrefs()->SetDict(proxy_config::prefs::kProxy,
                                            std::move(proxy_config_wpad));
  run_loop.Run();
  // Verify that the pref proxy is applied.
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            net::ProxyConfig::CreateAutoDetect().ToValue());

  // Set a system proxy via the AshNetworkSettingsService.
  crosapi::mojom::ProxyConfigPtr proxy_config =
      crosapi::mojom::ProxyConfig::New();
  crosapi::mojom::ProxySettingsPacPtr pac =
      crosapi::mojom::ProxySettingsPac::New();
  pac->pac_url = GURL(kPacUrl);
  proxy_config->proxy_settings =
      crosapi::mojom::ProxySettings::NewPac(std::move(pac));
  service_.SendProxyUpdate(std::move(proxy_config));
  base::RunLoop().RunUntilIdle();

  // Verify that the pref proxy is still applied.
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            net::ProxyConfig::CreateAutoDetect().ToValue());

  // Remove the pref and verify that the system proxy is applied.
  browser()->profile()->GetPrefs()->ClearPref(proxy_config::prefs::kProxy);
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            CrosapiProxyToNetProxy(std::move(proxy_config)).value().ToValue());
}

// Test that verifies that for secondary profiles, the system proxy sent from
// Ash-Chrome is only applied if the pref `kUseAshProxy` is true.
IN_PROC_BROWSER_TEST_F(ProxyConfigServiceLacrosTest, UseAshProxyPref) {
  if (!IsServiceAvailable())
    return;
  const base::Value expectedDirect = net::ProxyConfig::CreateDirect().ToValue();
  const base::Value expectedAutoDetect =
      net::ProxyConfig::CreateAutoDetect().ToValue();

  ResetProxyMonitoring();
  Profile& profile = CreateSecondaryProfile();
  SetupProxyMonitoring(&profile);

  profile.GetPrefs()->SetBoolean(prefs::kUseAshProxy, false);
  crosapi::mojom::ProxyConfigPtr proxy_config =
      crosapi::mojom::ProxyConfig::New();
  proxy_config->proxy_settings = crosapi::mojom::ProxySettings::NewWpad(
      crosapi::mojom::ProxySettingsWpad::New());
  service_.SendProxyUpdate(proxy_config.Clone());
  base::RunLoop().RunUntilIdle();
  // Verify that the system proxy is not applied.
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            expectedDirect);

  profile.GetPrefs()->SetBoolean(prefs::kUseAshProxy, true);
  // Verify that the system proxy is applied.
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            expectedAutoDetect);

  // Verify that the system proxy is not applied.
  profile.GetPrefs()->SetBoolean(prefs::kUseAshProxy, false);
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            expectedDirect);
}

// Verifies that the proxy is only applied incognito if it's not controlled by a
// an extension active in the main Lacros profile.
IN_PROC_BROWSER_TEST_F(ProxyConfigServiceLacrosTest, IncognitoProfile) {
  if (!IsServiceAvailable())
    return;
  const base::Value expectedDirect = net::ProxyConfig::CreateDirect().ToValue();
  const base::Value expectedAutoDetect =
      net::ProxyConfig::CreateAutoDetect().ToValue();

  ResetProxyMonitoring();
  auto* incognito_profile =
      browser()->profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
  SetupProxyMonitoring(incognito_profile);

  // kUseAshProxy should be ignored for the incognito profile.
  incognito_profile->GetPrefs()->SetBoolean(prefs::kUseAshProxy, true);

  crosapi::mojom::ProxyConfigPtr proxy_config =
      crosapi::mojom::ProxyConfig::New();
  proxy_config->proxy_settings = crosapi::mojom::ProxySettings::NewWpad(
      crosapi::mojom::ProxySettingsWpad::New());
  proxy_config->extension = crosapi::mojom::ExtensionControllingProxy::New();
  service_.SendProxyUpdate(proxy_config.Clone());
  base::RunLoop().RunUntilIdle();

  // Verify that the system proxy controlled by the pref is not applied.
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            expectedDirect);

  proxy_config->extension.reset();
  SendAshProxyUpdateAndWait(std::move(proxy_config));

  // Verify that the system proxy is applied.
  EXPECT_EQ(proxy_monitor_->cached_proxy_config_.value().ToValue(),
            expectedAutoDetect);
}

}  // namespace chromeos