| // 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/alternative_browser_driver.h" |
| |
| #include <windows.h> |
| |
| #include <ddeml.h> |
| #include <shellapi.h> |
| #include <shlobj.h> |
| #include <wininet.h> |
| |
| #include "base/files/file_path.h" |
| #include "base/process/launch.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/thread_pool.h" |
| #include "base/threading/scoped_blocking_call.h" |
| #include "base/win/registry.h" |
| #include "chrome/browser/browser_switcher/browser_switcher_prefs.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "content/public/browser/browser_task_traits.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "url/gurl.h" |
| |
| namespace browser_switcher { |
| |
| namespace { |
| |
| using LaunchCallback = AlternativeBrowserDriver::LaunchCallback; |
| |
| const wchar_t kUrlVarName[] = L"${url}"; |
| |
| const wchar_t kIExploreKey[] = |
| L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\IEXPLORE.EXE"; |
| const wchar_t kFirefoxKey[] = |
| L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\firefox.exe"; |
| // Opera does not register itself here for now but it's no harm to keep this. |
| const wchar_t kOperaKey[] = |
| L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\opera.exe"; |
| const wchar_t kSafariKey[] = |
| L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\safari.exe"; |
| const wchar_t kChromeKey[] = |
| L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe"; |
| const wchar_t kEdgeKey[] = |
| L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\msedge.exe"; |
| |
| const wchar_t kIExploreDdeHost[] = L"IExplore"; |
| |
| const wchar_t kChromeVarName[] = L"${chrome}"; |
| const wchar_t kIEVarName[] = L"${ie}"; |
| const wchar_t kFirefoxVarName[] = L"${firefox}"; |
| const wchar_t kOperaVarName[] = L"${opera}"; |
| const wchar_t kSafariVarName[] = L"${safari}"; |
| const wchar_t kEdgeVarName[] = L"${edge}"; |
| |
| // Case-insensitive, typical filenames for popular browsers' executables. |
| const wchar_t kChromeTypicalExecutable[] = L"chrome.exe"; |
| const wchar_t kIETypicalExecutable[] = L"iexplore.exe"; |
| const wchar_t kFirefoxTypicalExecutable[] = L"firefox.exe"; |
| const wchar_t kOperaTypicalExecutable[] = L"launcher.exe"; |
| const wchar_t kEdgeTypicalExecutable[] = L"msedge.exe"; |
| |
| struct BrowserVarMapping { |
| const wchar_t* var_name; |
| const wchar_t* registry_key; |
| const wchar_t* typical_executable; |
| const char* browser_name; |
| BrowserType browser_type; |
| }; |
| |
| const BrowserVarMapping kBrowserVarMappings[] = { |
| {kChromeVarName, kChromeKey, kChromeTypicalExecutable, "", |
| BrowserType::kChrome}, |
| {kIEVarName, kIExploreKey, kIETypicalExecutable, "Internet Explorer", |
| BrowserType::kIE}, |
| {kFirefoxVarName, kFirefoxKey, kFirefoxTypicalExecutable, "Mozilla Firefox", |
| BrowserType::kFirefox}, |
| {kOperaVarName, kOperaKey, kOperaTypicalExecutable, "Opera", |
| BrowserType::kOpera}, |
| {kSafariVarName, kSafariKey, L"", "Safari", BrowserType::kSafari}, |
| {kEdgeVarName, kEdgeKey, kEdgeTypicalExecutable, "Microsoft Edge", |
| BrowserType::kEdge}, |
| }; |
| |
| // DDE Callback function which is not used in our case at all. |
| HDDEDATA CALLBACK DdeCallback(UINT type, |
| UINT format, |
| HCONV handle, |
| HSZ string1, |
| HSZ string2, |
| HDDEDATA data, |
| ULONG_PTR data1, |
| ULONG_PTR data2) { |
| return NULL; |
| } |
| |
| void PercentEncodeCommas(std::wstring* url) { |
| size_t pos = url->find(L","); |
| while (pos != std::wstring::npos) { |
| url->replace(pos, 1, L"%2C"); |
| pos = url->find(L",", pos); |
| } |
| } |
| |
| void PercentUnencodeQuotes(std::wstring* url) { |
| base::ReplaceSubstringsAfterOffset(url, 0, L"%27", L"'"); |
| } |
| |
| std::wstring GetBrowserLocation(const wchar_t* regkey_name) { |
| DCHECK(regkey_name); |
| base::win::RegKey key; |
| if (ERROR_SUCCESS != key.Open(HKEY_LOCAL_MACHINE, regkey_name, KEY_READ) && |
| ERROR_SUCCESS != key.Open(HKEY_CURRENT_USER, regkey_name, KEY_READ)) { |
| LOG(ERROR) << "Could not open registry key " << regkey_name |
| << "! Error Code:" << GetLastError(); |
| return std::wstring(); |
| } |
| std::wstring location; |
| if (ERROR_SUCCESS != key.ReadValue(NULL, &location)) |
| return std::wstring(); |
| return location; |
| } |
| |
| const BrowserVarMapping* FindBrowserMapping(base::WStringPiece path, |
| bool compare_typical_executable) { |
| // If |compare_typical_executable| is true: also look at executable filenames, |
| // to reduce false-negatives when the path is specified explicitly by the |
| // admin. |
| if (path.empty()) |
| path = kIEVarName; |
| for (const auto& mapping : kBrowserVarMappings) { |
| if (!path.compare(mapping.var_name) || |
| (compare_typical_executable && *mapping.typical_executable && |
| base::EndsWith(path, mapping.typical_executable, |
| base::CompareCase::INSENSITIVE_ASCII))) { |
| return &mapping; |
| } |
| } |
| return nullptr; |
| } |
| |
| void ExpandPresetBrowsers(std::wstring* str) { |
| const auto* mapping = FindBrowserMapping(*str, false); |
| if (mapping) |
| *str = GetBrowserLocation(mapping->registry_key); |
| } |
| |
| bool ExpandUrlVarName(std::wstring* arg, const std::wstring& url_spec) { |
| size_t url_index = arg->find(kUrlVarName); |
| if (url_index == std::wstring::npos) |
| return false; |
| arg->replace(url_index, wcslen(kUrlVarName), url_spec); |
| return true; |
| } |
| |
| void ExpandEnvironmentVariables(std::wstring* arg) { |
| DWORD expanded_size = 0; |
| expanded_size = ::ExpandEnvironmentStrings(arg->c_str(), NULL, expanded_size); |
| if (expanded_size == 0) |
| return; |
| |
| // The expected buffer length as defined in MSDN is chars + null + 1. |
| std::unique_ptr<wchar_t[]> out(new wchar_t[expanded_size + 2]); |
| expanded_size = |
| ::ExpandEnvironmentStrings(arg->c_str(), out.get(), expanded_size); |
| if (expanded_size != 0) |
| *arg = out.get(); |
| } |
| |
| void AppendCommandLineArguments(base::CommandLine* cmd_line, |
| const std::vector<std::string>& raw_args, |
| const GURL& url) { |
| std::wstring url_spec = base::UTF8ToWide(url.spec()); |
| // IE has some quirks with quote characters. Send them verbatim instead |
| // of percent-encoding them. |
| PercentUnencodeQuotes(&url_spec); |
| std::vector<std::wstring> command_line; |
| bool contains_url = false; |
| for (const auto& arg : raw_args) { |
| std::wstring expanded_arg = base::UTF8ToWide(arg); |
| ExpandEnvironmentVariables(&expanded_arg); |
| if (ExpandUrlVarName(&expanded_arg, url_spec)) |
| contains_url = true; |
| cmd_line->AppendArgNative(expanded_arg); |
| } |
| if (!contains_url) |
| cmd_line->AppendArgNative(url_spec); |
| } |
| |
| bool IsInternetExplorer(base::StringPiece path) { |
| // We don't treat IExplore.exe as Internet Explorer here. This way, admins can |
| // set |AlternativeBrowserPath| to IExplore.exe to disable DDE, if it's |
| // causing issues or slowness. |
| return path.empty() || base::EqualsASCII(base::as_u16cstr(kIEVarName), path); |
| } |
| |
| bool TryLaunchWithDde(const GURL& url, const std::string& path) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::WILL_BLOCK); |
| if (!IsInternetExplorer(path)) |
| return false; |
| |
| DWORD dde_instance = 0; |
| UINT dml_error = |
| DdeInitialize(&dde_instance, DdeCallback, CBF_FAIL_ALLSVRXACTIONS, 0); |
| if (dml_error != DMLERR_NO_ERROR) { |
| VLOG(1) << "DdeInitialize() failed: " << dml_error; |
| return false; |
| } |
| |
| bool success = false; |
| HCONV openurl_service_instance; |
| HCONV activate_service_instance; |
| { |
| HSZ service = |
| DdeCreateStringHandle(dde_instance, kIExploreDdeHost, CP_WINUNICODE); |
| HSZ openurl_topic = |
| DdeCreateStringHandle(dde_instance, L"WWW_OpenURL", CP_WINUNICODE); |
| HSZ activate_topic = |
| DdeCreateStringHandle(dde_instance, L"WWW_Activate", CP_WINUNICODE); |
| openurl_service_instance = |
| DdeConnect(dde_instance, service, openurl_topic, NULL); |
| activate_service_instance = |
| DdeConnect(dde_instance, service, activate_topic, NULL); |
| DdeFreeStringHandle(dde_instance, service); |
| DdeFreeStringHandle(dde_instance, openurl_topic); |
| DdeFreeStringHandle(dde_instance, activate_topic); |
| } |
| |
| if (openurl_service_instance) { |
| // Percent-encode commas and spaces because those mean something else |
| // for the WWW_OpenURL verb and the url is trimmed on the first one. |
| // Spaces are already encoded by GURL. |
| std::wstring encoded_url(base::UTF8ToWide(url.spec())); |
| PercentUnencodeQuotes(&encoded_url); |
| PercentEncodeCommas(&encoded_url); |
| |
| success = |
| DdeClientTransaction( |
| reinterpret_cast<LPBYTE>(const_cast<wchar_t*>(encoded_url.data())), |
| encoded_url.size() * sizeof(wchar_t), openurl_service_instance, 0, |
| 0, XTYP_EXECUTE, TIMEOUT_ASYNC, NULL) != 0; |
| DdeDisconnect(openurl_service_instance); |
| if (activate_service_instance) { |
| if (success) { |
| // Bring window to the front. |
| wchar_t cmd[] = L"0xFFFFFFFF,0x0"; |
| DdeClientTransaction(reinterpret_cast<LPBYTE>(cmd), sizeof(cmd), |
| activate_service_instance, 0, 0, XTYP_EXECUTE, |
| TIMEOUT_ASYNC, NULL); |
| } |
| DdeDisconnect(activate_service_instance); |
| } |
| } |
| dml_error = ::DdeGetLastError(dde_instance); |
| if (dml_error != DMLERR_NO_ERROR) |
| VLOG(1) << "DDE error: " << dml_error; |
| DdeUninitialize(dde_instance); |
| return success; |
| } |
| |
| base::CommandLine CreateCommandLine(const GURL& url, |
| const std::string& utf8_path, |
| const std::vector<std::string>& params) { |
| std::wstring path = base::UTF8ToWide(utf8_path); |
| ExpandPresetBrowsers(&path); |
| ExpandEnvironmentVariables(&path); |
| base::CommandLine cmd_line(std::vector<std::wstring>{path}); |
| |
| AppendCommandLineArguments(&cmd_line, params, url); |
| |
| return cmd_line; |
| } |
| |
| bool TryLaunchWithExec(const GURL& url, |
| const std::string& path, |
| const std::vector<std::string>& args) { |
| base::ScopedBlockingCall scoped_blocking_call(FROM_HERE, |
| base::BlockingType::MAY_BLOCK); |
| CHECK(url.SchemeIsHTTPOrHTTPS() || url.SchemeIsFile()); |
| |
| auto cmd_line = CreateCommandLine(url, path, args); |
| |
| base::LaunchOptions options; |
| if (!base::LaunchProcess(cmd_line, options).IsValid()) { |
| LOG(ERROR) << "Could not start the alternative browser! Error: " |
| << GetLastError(); |
| return false; |
| } |
| return true; |
| } |
| |
| void TryLaunchBlocking(GURL url, |
| std::string path, |
| std::vector<std::string> params, |
| LaunchCallback cb) { |
| const bool success = |
| (TryLaunchWithDde(url, path) || TryLaunchWithExec(url, path, params)); |
| content::GetUIThreadTaskRunner({})->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](bool success, LaunchCallback cb) { std::move(cb).Run(success); }, |
| success, std::move(cb))); |
| } |
| |
| } // namespace |
| |
| AlternativeBrowserDriver::~AlternativeBrowserDriver() = default; |
| |
| AlternativeBrowserDriverImpl::AlternativeBrowserDriverImpl( |
| const BrowserSwitcherPrefs* prefs) |
| : prefs_(prefs) {} |
| |
| AlternativeBrowserDriverImpl::~AlternativeBrowserDriverImpl() = default; |
| |
| void AlternativeBrowserDriverImpl::TryLaunch(const GURL& url, |
| LaunchCallback cb) { |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| VLOG(2) << "Launching alternative browser..."; |
| VLOG(2) << " path = " << prefs_->GetAlternativeBrowserPath(); |
| VLOG(2) << " url = " << url.spec(); |
| base::ThreadPool::PostTask( |
| FROM_HERE, |
| {base::MayBlock(), base::TaskPriority::USER_BLOCKING, |
| base::TaskShutdownBehavior::BLOCK_SHUTDOWN}, |
| base::BindOnce(&TryLaunchBlocking, url, |
| prefs_->GetAlternativeBrowserPath(), |
| prefs_->GetAlternativeBrowserParameters(), std::move(cb))); |
| } |
| |
| std::string AlternativeBrowserDriverImpl::GetBrowserName() const { |
| std::wstring path = base::UTF8ToWide(prefs_->GetAlternativeBrowserPath()); |
| const auto* mapping = FindBrowserMapping(path, false); |
| return mapping ? mapping->browser_name : "alternative browser"; |
| } |
| |
| BrowserType AlternativeBrowserDriverImpl::GetBrowserType() const { |
| std::wstring path = base::UTF8ToWide(prefs_->GetAlternativeBrowserPath()); |
| const auto* mapping = FindBrowserMapping(path, true); |
| return mapping ? mapping->browser_type : BrowserType::kUnknown; |
| } |
| |
| base::CommandLine AlternativeBrowserDriverImpl::CreateCommandLine( |
| const GURL& url) { |
| return browser_switcher::CreateCommandLine( |
| url, prefs_->GetAlternativeBrowserPath(), |
| prefs_->GetAlternativeBrowserParameters()); |
| } |
| |
| } // namespace browser_switcher |