| // 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 "chrome/browser/browser_switcher/browser_switcher_service.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/syslog_logging.h" |
| #include "chrome/browser/browser_switcher/alternative_browser_driver.h" |
| #include "chrome/browser/browser_switcher/browser_switcher_prefs.h" |
| #include "chrome/browser/browser_switcher/browser_switcher_sitelist.h" |
| #include "chrome/browser/browser_switcher/ieem_sitelist_parser.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/file_url_loader.h" |
| #include "content/public/browser/shared_cors_origin_access_list.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "net/base/load_flags.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "services/network/public/cpp/resource_request.h" |
| |
| namespace browser_switcher { |
| |
| namespace { |
| |
| // How long to wait after |BrowserSwitcherService| is created before initiating |
| // the sitelist fetch. Non-zero values are used for testing. |
| // |
| // TODO(nicolaso): get rid of this. |
| const base::TimeDelta kFetchSitelistDelay = base::TimeDelta(); |
| |
| // How long to wait after a fetch to re-fetch the sitelist to keep it fresh. |
| const base::TimeDelta kRefreshSitelistDelay = base::Minutes(30); |
| |
| // How many times to re-try fetching the XML file for the sitelist. |
| const int kFetchNumRetries = 1; |
| |
| constexpr net::NetworkTrafficAnnotationTag traffic_annotation = |
| net::DefineNetworkTrafficAnnotation("browser_switcher_ieem_sitelist", R"( |
| semantics { |
| sender: "Legacy Browser Support " |
| description: |
| "Legacy Browser Support may download Internet Explorer's " |
| "Enterprise Mode SiteList XML, to load the list of URLs to open in " |
| "an alternative browser. This is often on the organization's " |
| "intranet. For more information on Internet Explorer's Enterprise " |
| "Mode, see: " |
| "https://docs.microsoft.com/internet-explorer/ie11-deploy-guide" |
| "/what-is-enterprise-mode" |
| trigger: |
| "1 minute after browser startup, and then refreshes every 30 " |
| "minutes afterwards. Only happens if Legacy Browser Support is " |
| "enabled via enterprise policies." |
| data: |
| "Up to 3 (plus retries) HTTP or HTTPS GET requests to the URLs " |
| "configured in Internet Explorer's SiteList policy, and Chrome's " |
| "BrowserSwitcherExternalSitelistUrl and " |
| "BrowserSwitcherExternalGreylistUrl policies." |
| destination: OTHER |
| destination_other: |
| "URL configured in Internet Explorer's SiteList policy, and URLs " |
| "configured in Chrome's BrowserSwitcherExternalSitelistUrl and " |
| "BrowserSwitcherExternalGreylistUrl policies." |
| } |
| policy { |
| cookies_allowed: NO |
| setting: "This feature cannot be disabled by settings." |
| chrome_policy: { |
| BrowserSwitcherEnabled: { |
| BrowserSwitcherEnabled: false |
| } |
| } |
| })"); |
| |
| } // namespace |
| |
| RulesetSource::RulesetSource( |
| std::string pref_name_, |
| GURL url_, |
| bool contains_inverted_rules_, |
| base::OnceCallback<void(ParsedXml xml)> parsed_callback_) |
| : pref_name(std::move(pref_name_)), |
| url(std::move(url_)), |
| contains_inverted_rules(contains_inverted_rules_), |
| parsed_callback(std::move(parsed_callback_)) {} |
| |
| RulesetSource::RulesetSource(RulesetSource&&) = default; |
| |
| RulesetSource::~RulesetSource() = default; |
| |
| XmlDownloader::XmlDownloader(Profile* profile, |
| BrowserSwitcherService* service, |
| base::TimeDelta first_fetch_delay, |
| base::RepeatingCallback<void()> all_done_callback) |
| : service_(service), all_done_callback_(std::move(all_done_callback)) { |
| file_url_factory_.Bind( |
| content::CreateFileURLLoaderFactory(base::FilePath(), nullptr)); |
| other_url_factory_ = profile->GetDefaultStoragePartition() |
| ->GetURLLoaderFactoryForBrowserProcess(); |
| |
| sources_ = service_->GetRulesetSources(); |
| |
| for (auto& source : sources_) { |
| if (!source.url.is_valid()) |
| DoneParsing(&source, ParsedXml({})); |
| } |
| |
| // Fetch in 1 minute. |
| ScheduleRefresh(first_fetch_delay); |
| } |
| |
| XmlDownloader::~XmlDownloader() = default; |
| |
| bool XmlDownloader::HasValidSources() const { |
| return std::any_of( |
| sources_.begin(), sources_.end(), |
| [](const RulesetSource& source) { return source.url.is_valid(); }); |
| } |
| |
| base::Time XmlDownloader::last_refresh_time() const { |
| return last_refresh_time_; |
| } |
| |
| base::Time XmlDownloader::next_refresh_time() const { |
| return next_refresh_time_; |
| } |
| |
| void XmlDownloader::FetchXml() { |
| for (auto& source : sources_) { |
| if (!source.url.is_valid()) { |
| DoneParsing(&source, ParsedXml({})); |
| continue; |
| } |
| |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = source.url; |
| request->load_flags = net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE; |
| request->credentials_mode = network::mojom::CredentialsMode::kInclude; |
| request->priority = net::IDLE; |
| source.url_loader = network::SimpleURLLoader::Create(std::move(request), |
| traffic_annotation); |
| source.url_loader->SetRetryOptions( |
| kFetchNumRetries, |
| network::SimpleURLLoader::RetryMode::RETRY_ON_NETWORK_CHANGE); |
| source.url_loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| GetURLLoaderFactoryForURL(source.url), |
| base::BindOnce(&XmlDownloader::ParseXml, weak_ptr_factory_.GetWeakPtr(), |
| base::Unretained(&source))); |
| } |
| } |
| |
| network::mojom::URLLoaderFactory* XmlDownloader::GetURLLoaderFactoryForURL( |
| const GURL& url) { |
| if (url.SchemeIsFile()) |
| return file_url_factory_.get(); |
| return other_url_factory_.get(); |
| } |
| |
| void XmlDownloader::ParseXml(RulesetSource* source, |
| std::unique_ptr<std::string> bytes) { |
| if (!bytes) { |
| DoneParsing(source, ParsedXml({}, "could not fetch XML")); |
| return; |
| } |
| ParseIeemXml(*bytes, base::BindOnce(&XmlDownloader::DoneParsing, |
| weak_ptr_factory_.GetWeakPtr(), |
| base::Unretained(source))); |
| } |
| |
| void XmlDownloader::DoneParsing(RulesetSource* source, ParsedXml xml) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| |
| // Greylists can't contain any negative rules, so remove the leading "!". |
| if (source->contains_inverted_rules) { |
| for (auto& rule : xml.rules) { |
| if (base::StartsWith(rule, "!", base::CompareCase::SENSITIVE)) |
| rule.erase(0, 1); |
| } |
| } |
| |
| if (xml.error) |
| LOG(ERROR) << *xml.error; |
| std::move(source->parsed_callback).Run(std::move(xml)); |
| |
| // Run the "all done" callback if this was the last ruleset. |
| counter_++; |
| DCHECK(counter_ <= sources_.size()); |
| if (counter_ == sources_.size()) { |
| all_done_callback_.Run(); |
| if (HasValidSources()) |
| last_refresh_time_ = base::Time::Now(); |
| ScheduleRefresh(service_->refresh_delay()); |
| } |
| } |
| |
| void XmlDownloader::ScheduleRefresh(base::TimeDelta delay) { |
| // Avoid doing unnecessary work. |
| if (!HasValidSources()) |
| return; |
| |
| // Refresh in 30 minutes, so the sitelists are never too stale. |
| base::ThreadTaskRunnerHandle::Get()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&XmlDownloader::Refresh, weak_ptr_factory_.GetWeakPtr()), |
| delay); |
| next_refresh_time_ = base::Time::Now() + delay; |
| } |
| |
| void XmlDownloader::Refresh() { |
| sources_ = service_->GetRulesetSources(); |
| counter_ = 0; |
| FetchXml(); |
| } |
| |
| BrowserSwitcherService::BrowserSwitcherService(Profile* profile) |
| : profile_(profile), |
| prefs_(profile), |
| driver_(new AlternativeBrowserDriverImpl(&prefs_)), |
| sitelist_(new BrowserSwitcherSitelistImpl(&prefs_)) { |
| prefs_subscription_ = |
| prefs().RegisterPrefsChangedCallback(base::BindRepeating( |
| &BrowserSwitcherService::OnBrowserSwitcherPrefsChanged, |
| base::Unretained(this))); |
| |
| if (prefs_.IsEnabled()) { |
| UMA_HISTOGRAM_ENUMERATION("BrowserSwitcher.AlternativeBrowser", |
| driver_->GetBrowserType()); |
| } |
| } |
| |
| BrowserSwitcherService::~BrowserSwitcherService() = default; |
| |
| void BrowserSwitcherService::Init() { |
| LoadRulesFromPrefs(); |
| StartDownload(fetch_delay()); |
| } |
| |
| void BrowserSwitcherService::OnAllRulesetsLoadedForTesting( |
| base::OnceCallback<void()> cb) { |
| all_rulesets_loaded_callback_for_testing_ = std::move(cb); |
| } |
| |
| void BrowserSwitcherService::StartDownload(base::TimeDelta delay) { |
| // This destroys the previous XmlDownloader, which cancels any scheduled |
| // refresh operations. |
| sitelist_downloader_ = std::make_unique<XmlDownloader>( |
| profile_, this, delay, |
| base::BindRepeating(&BrowserSwitcherService::OnAllRulesetsParsed, |
| base::Unretained(this))); |
| } |
| |
| void BrowserSwitcherService::Shutdown() { |
| prefs_.Shutdown(); |
| } |
| |
| AlternativeBrowserDriver* BrowserSwitcherService::driver() { |
| return driver_.get(); |
| } |
| |
| BrowserSwitcherSitelist* BrowserSwitcherService::sitelist() { |
| return sitelist_.get(); |
| } |
| |
| BrowserSwitcherPrefs& BrowserSwitcherService::prefs() { |
| return prefs_; |
| } |
| |
| Profile* BrowserSwitcherService::profile() { |
| return profile_; |
| } |
| |
| XmlDownloader* BrowserSwitcherService::sitelist_downloader() { |
| return sitelist_downloader_.get(); |
| } |
| |
| base::TimeDelta BrowserSwitcherService::fetch_delay() { |
| return fetch_delay_; |
| } |
| |
| base::TimeDelta BrowserSwitcherService::refresh_delay() { |
| return refresh_delay_; |
| } |
| |
| void BrowserSwitcherService::SetDriverForTesting( |
| std::unique_ptr<AlternativeBrowserDriver> driver) { |
| driver_ = std::move(driver); |
| } |
| |
| void BrowserSwitcherService::SetSitelistForTesting( |
| std::unique_ptr<BrowserSwitcherSitelist> sitelist) { |
| sitelist_ = std::move(sitelist); |
| } |
| |
| std::vector<RulesetSource> BrowserSwitcherService::GetRulesetSources() { |
| std::vector<RulesetSource> sources; |
| |
| GURL sitelist_url = prefs_.GetExternalSitelistUrl(); |
| sources.emplace_back( |
| prefs::kExternalSitelistUrl, sitelist_url, /* invert_rules */ false, |
| base::BindOnce(&BrowserSwitcherService::OnExternalSitelistParsed, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| GURL greylist_url = prefs_.GetExternalGreylistUrl(); |
| sources.emplace_back( |
| prefs::kExternalGreylistUrl, greylist_url, /* invert_rules */ true, |
| base::BindOnce(&BrowserSwitcherService::OnExternalGreylistParsed, |
| weak_ptr_factory_.GetWeakPtr())); |
| |
| return sources; |
| } |
| |
| void BrowserSwitcherService::LoadRulesFromPrefs() { |
| if (prefs().GetExternalSitelistUrl().is_valid()) |
| sitelist()->SetExternalSitelist( |
| ParsedXml(prefs().GetCachedExternalSitelist(), absl::nullopt)); |
| if (prefs().GetExternalGreylistUrl().is_valid()) |
| sitelist()->SetExternalGreylist( |
| ParsedXml(prefs().GetCachedExternalGreylist(), absl::nullopt)); |
| } |
| |
| void BrowserSwitcherService::OnAllRulesetsParsed() { |
| callback_list_.Notify(this); |
| if (all_rulesets_loaded_callback_for_testing_) |
| std::move(all_rulesets_loaded_callback_for_testing_).Run(); |
| } |
| |
| base::CallbackListSubscription |
| BrowserSwitcherService::RegisterAllRulesetsParsedCallback( |
| AllRulesetsParsedCallback callback) { |
| return callback_list_.Add(callback); |
| } |
| |
| void BrowserSwitcherService::OnBrowserSwitcherPrefsChanged( |
| BrowserSwitcherPrefs* prefs, |
| const std::vector<std::string>& changed_prefs) { |
| // Record |BrowserSwitcher.AlternativeBrowser| when the |
| // |BrowserSwitcherEnabled| or |AlternativeBrowserPath| policies change. |
| bool should_record_metrics = |
| changed_prefs.end() != |
| std::find_if(changed_prefs.begin(), changed_prefs.end(), |
| [](const std::string& pref) { |
| return pref == prefs::kEnabled || |
| pref == prefs::kAlternativeBrowserPath; |
| }); |
| if (should_record_metrics && prefs_.IsEnabled()) { |
| UMA_HISTOGRAM_ENUMERATION("BrowserSwitcher.AlternativeBrowser", |
| driver_->GetBrowserType()); |
| } |
| |
| auto sources = GetRulesetSources(); |
| |
| // Re-download if one of the URLs changed. O(n^2), with n <= 3. |
| bool should_redownload = std::any_of( |
| sources.begin(), sources.end(), |
| [&changed_prefs](const RulesetSource& source) { |
| return (std::find(changed_prefs.begin(), changed_prefs.end(), |
| source.pref_name) != changed_prefs.end()); |
| }); |
| |
| if (should_redownload) |
| StartDownload(fetch_delay()); |
| } |
| |
| void BrowserSwitcherService::OnExternalSitelistParsed(ParsedXml xml) { |
| if (xml.error) { |
| SYSLOG(INFO) << "Unable to parse IEEM SiteList: " << *xml.error; |
| } else { |
| VLOG(2) << "Done parsing external SiteList for sitelist rules. " |
| << "Applying rules to future navigations."; |
| |
| if (prefs().GetExternalSitelistUrl().is_valid()) |
| prefs().SetCachedExternalSitelist(xml.rules); |
| |
| sitelist()->SetExternalSitelist(std::move(xml)); |
| } |
| } |
| |
| void BrowserSwitcherService::OnExternalGreylistParsed(ParsedXml xml) { |
| if (xml.error) { |
| SYSLOG(INFO) << "Unable to parse IEEM SiteList: " << *xml.error; |
| } else { |
| VLOG(2) << "Done parsing external SiteList for greylist rules. " |
| << "Applying rules to future navigations."; |
| |
| if (prefs().GetExternalGreylistUrl().is_valid()) |
| prefs().SetCachedExternalGreylist(xml.rules); |
| |
| sitelist()->SetExternalGreylist(std::move(xml)); |
| } |
| } |
| |
| base::TimeDelta BrowserSwitcherService::fetch_delay_ = kFetchSitelistDelay; |
| base::TimeDelta BrowserSwitcherService::refresh_delay_ = kRefreshSitelistDelay; |
| |
| // static |
| void BrowserSwitcherService::SetFetchDelayForTesting(base::TimeDelta delay) { |
| fetch_delay_ = delay; |
| } |
| |
| // static |
| void BrowserSwitcherService::SetRefreshDelayForTesting(base::TimeDelta delay) { |
| refresh_delay_ = delay; |
| } |
| |
| } // namespace browser_switcher |