chromium/chrome/browser/ash/net/ash_proxy_monitor.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 <stdint.h>

#include "ash/constants/ash_pref_names.h"
#include "base/logging.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_helper.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_state.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/onc/network_onc_utils.h"
#include "chromeos/ash/components/network/proxy/proxy_config_service_impl.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/proxy_config/proxy_config_pref_names.h"
#include "components/user_manager/user_manager.h"

namespace {

const char kPrefExtensionNameKey[] = "extension_name_key";
const char kPrefExtensionIdKey[] = "extension_id_key";
const char kPrefExtensionCanDisabledKey[] = "can_be_disabled_key";

// TODO(acostinas,b/267158784) Remove this method after the new version of
// Lacros, which sets the proxy via the mojo Prefs service, is deployed to Ash
// (after minimum four releases to keep up with the version skew).
// When the proxy is set by an extension in the primary profile, the
// kProxy pref used to be stored in the user store. Now the pref is stored in
// the standalone browser store but we need to ensure that the old value is not
// lingering in the user store. This pref cannot be set by the user in the UI
// because proxies can only be set per network in Chrome OS.
void CleanupPrefFromUserStore(Profile* profile) {
  if (!profile) {
    return;
  }
  const PrefService::Preference* pref =
      profile->GetPrefs()->FindPreference(proxy_config::prefs::kProxy);
  DCHECK(pref) << "kProxy pref called before being registered.";

  if (!pref->HasUserSetting()) {
    return;
  }
  profile->GetPrefs()->ClearPref(proxy_config::prefs::kProxy);
}
}  // namespace

namespace ash {

AshProxyMonitor::ExtensionMetadata::ExtensionMetadata(const std::string& name,
                                                      const std::string& id,
                                                      bool can_be_disabled) {
  this->name = name;
  this->id = id;
  this->can_be_disabled = can_be_disabled;
}

AshProxyMonitor::AshProxyMonitor(PrefService* local_state,
                                 ProfileManager* profile_manager)
    : local_state_(local_state), profile_manager_(profile_manager) {
  if (profile_manager_) {
    profile_manager_observation_.Observe(profile_manager_);
  }
  // Uninitialized in unit_tests.
  if (ash::NetworkHandler::IsInitialized()) {
    network_state_handler_observer_.Observe(
        ash::NetworkHandler::Get()->network_state_handler());
  }
}

AshProxyMonitor::~AshProxyMonitor() = default;

void AshProxyMonitor::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void AshProxyMonitor::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

// static
void AshProxyMonitor::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(
      ash::prefs::kLacrosProxyControllingExtension);
}

void AshProxyMonitor::DefaultNetworkChanged(const ash::NetworkState* network) {
  if (!network) {
    cached_wpad_url_ = GURL();
    return;
  }
  if (cached_wpad_url_ != network->GetWebProxyAutoDiscoveryUrl()) {
    OnProxyChanged(network->GetWebProxyAutoDiscoveryUrl());
    return;
  }
  OnProxyChanged(std::nullopt);
}

void AshProxyMonitor::OnShuttingDown() {
  network_state_handler_observer_.Reset();
}

void AshProxyMonitor::OnProxyChanged(std::optional<GURL> wpad_url) {
  if (!primary_profile_) {
    return;
  }

  bool send_update = false;

  if (wpad_url.has_value() && wpad_url != cached_wpad_url_) {
    cached_wpad_url_ = wpad_url;
    send_update = true;
  }

  std::unique_ptr<ProxyConfigDictionary> proxy_dict =
      ProxyConfigServiceImpl::GetActiveProxyConfigDictionary(
          primary_profile_->GetPrefs(), local_state_);
  if (!proxy_dict) {
    proxy_dict = std::make_unique<ProxyConfigDictionary>(
        ProxyConfigDictionary::CreateDirect());
  }

  if (!cached_proxy_config_ ||
      cached_proxy_config_->GetDictionary() != proxy_dict->GetDictionary()) {
    cached_proxy_config_ = std::move(proxy_dict);
    send_update = true;
  }

  if (send_update) {
    NotifyObservers();
  }
}

void AshProxyMonitor::NotifyObservers() {
  for (auto& observer : observers_) {
    observer.OnProxyChanged();
  }
}

void AshProxyMonitor::OnProfileAdded(Profile* profile) {
  if (!user_manager::UserManager::Get()->IsPrimaryUser(
          BrowserContextHelper::Get()->GetUserByBrowserContext(profile))) {
    return;
  }
  primary_profile_ = profile;

  CleanupPrefFromUserStore(primary_profile_);
  profile_prefs_registrar_ = std::make_unique<PrefChangeRegistrar>();
  profile_prefs_registrar_->Init(primary_profile_->GetPrefs());
  // This can be triggered by user policy changes or extensions running in Ash
  // or Lacros. The `wpad_url` can only be configured by DHCP and/or DNS
  // discovery methods.
  profile_prefs_registrar_->Add(
      proxy_config::prefs::kProxy,
      base::BindRepeating(&AshProxyMonitor::OnProxyChanged,
                          base::Unretained(this), /*wpad_url*/ std::nullopt));
}

void AshProxyMonitor::OnProfileMarkedForPermanentDeletion(Profile* profile) {
  if (!primary_profile_ || primary_profile_ != profile) {
    return;
  }
  profile_prefs_registrar_.reset();
  primary_profile_ = nullptr;
}

void AshProxyMonitor::OnProfileManagerDestroying() {
  profile_prefs_registrar_.reset();
  primary_profile_ = nullptr;

  if (!profile_manager_) {
    return;
  }
  profile_manager_observation_.Reset();
  profile_manager_ = nullptr;
}

void AshProxyMonitor::SetProfileForTesting(Profile* profile) {
  OnProfileAdded(profile);
}

ProxyConfigDictionary* AshProxyMonitor::GetLatestProxyConfig() const {
  return cached_proxy_config_.get();
}

GURL AshProxyMonitor::GetLatestWpadUrl() const {
  return cached_wpad_url_.value_or(GURL());
}

bool AshProxyMonitor::IsLacrosExtensionControllingProxy() const {
  if (!primary_profile_) {
    return false;
  }
  auto* pref =
      primary_profile_->GetPrefs()->FindPreference(proxy_config::prefs::kProxy);
  return pref && pref->IsStandaloneBrowserControlled();
}

void AshProxyMonitor::SetLacrosExtensionControllingProxyInfo(
    const std::string& name,
    const std::string& id,
    bool can_be_disabled) {
  DCHECK(primary_profile_) << "The primary profile is not initialized";
  CleanupPrefFromUserStore(primary_profile_);
  primary_profile_->GetPrefs()->SetDict(
      ash::prefs::kLacrosProxyControllingExtension,
      base::Value::Dict()
          .Set(kPrefExtensionNameKey, name)
          .Set(kPrefExtensionIdKey, id)
          .Set(kPrefExtensionCanDisabledKey, can_be_disabled));
  NotifyObservers();
}

void AshProxyMonitor::ClearLacrosExtensionControllingProxyInfo() {
  DCHECK(primary_profile_) << "The primary profile is not initialized";
  CleanupPrefFromUserStore(primary_profile_);
  primary_profile_->GetPrefs()->ClearPref(
      ash::prefs::kLacrosProxyControllingExtension);
  NotifyObservers();
}

std::optional<AshProxyMonitor::ExtensionMetadata>
AshProxyMonitor::GetLacrosExtensionControllingTheProxy() const {
  if (!IsLacrosExtensionControllingProxy()) {
    return std::nullopt;
  }
  const base::Value::Dict& dictionary = primary_profile_->GetPrefs()->GetDict(
      ash::prefs::kLacrosProxyControllingExtension);
  const std::string* extension_name =
      dictionary.FindString(kPrefExtensionNameKey);

  if (!extension_name) {
    // Ash received the proxy config from Lacros via the Prefs service but the
    // metadata of the extension, which is sent from Lacros via the
    // NetworkSettings service, is not yet received.
    return std::nullopt;
  }

  const std::string* extension_id = dictionary.FindString(kPrefExtensionIdKey);
  return ExtensionMetadata(
      extension_name ? *extension_name : std::string(),
      extension_id ? *extension_id : std::string(),
      dictionary.FindBool(kPrefExtensionCanDisabledKey).value_or(false));
}

}  // namespace ash