1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "chrome/browser/browser_switcher/alternative_browser_driver.h"
6
7 #include <windows.h>
8
9 #include <ddeml.h>
10 #include <shellapi.h>
11 #include <shlobj.h>
12 #include <wininet.h>
13
14 #include "base/files/file_path.h"
15 #include "base/process/launch.h"
16 #include "base/strings/string_piece.h"
17 #include "base/strings/string_util.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "base/task/thread_pool.h"
20 #include "base/threading/scoped_blocking_call.h"
21 #include "base/win/registry.h"
22 #include "chrome/browser/browser_switcher/browser_switcher_prefs.h"
23 #include "chrome/grit/generated_resources.h"
24 #include "content/public/browser/browser_task_traits.h"
25 #include "content/public/browser/browser_thread.h"
26 #include "url/gurl.h"
27
28 namespace browser_switcher {
29
30 namespace {
31
32 using LaunchCallback = AlternativeBrowserDriver::LaunchCallback;
33
34 const wchar_t kUrlVarName[] = L"${url}";
35
36 const wchar_t kIExploreKey[] =
37 L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\IEXPLORE.EXE";
38 const wchar_t kFirefoxKey[] =
39 L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\firefox.exe";
40 // Opera does not register itself here for now but it's no harm to keep this.
41 const wchar_t kOperaKey[] =
42 L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\opera.exe";
43 const wchar_t kSafariKey[] =
44 L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\safari.exe";
45 const wchar_t kChromeKey[] =
46 L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe";
47 const wchar_t kEdgeKey[] =
48 L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\msedge.exe";
49
50 const wchar_t kIExploreDdeHost[] = L"IExplore";
51
52 const wchar_t kChromeVarName[] = L"${chrome}";
53 const wchar_t kIEVarName[] = L"${ie}";
54 const wchar_t kFirefoxVarName[] = L"${firefox}";
55 const wchar_t kOperaVarName[] = L"${opera}";
56 const wchar_t kSafariVarName[] = L"${safari}";
57 const wchar_t kEdgeVarName[] = L"${edge}";
58
59 // Case-insensitive, typical filenames for popular browsers' executables.
60 const wchar_t kChromeTypicalExecutable[] = L"chrome.exe";
61 const wchar_t kIETypicalExecutable[] = L"iexplore.exe";
62 const wchar_t kFirefoxTypicalExecutable[] = L"firefox.exe";
63 const wchar_t kOperaTypicalExecutable[] = L"launcher.exe";
64 const wchar_t kEdgeTypicalExecutable[] = L"msedge.exe";
65
66 struct BrowserVarMapping {
67 const wchar_t* var_name;
68 const wchar_t* registry_key;
69 const wchar_t* typical_executable;
70 const char* browser_name;
71 BrowserType browser_type;
72 };
73
74 const BrowserVarMapping kBrowserVarMappings[] = {
75 {kChromeVarName, kChromeKey, kChromeTypicalExecutable, "",
76 BrowserType::kChrome},
77 {kIEVarName, kIExploreKey, kIETypicalExecutable, "Internet Explorer",
78 BrowserType::kIE},
79 {kFirefoxVarName, kFirefoxKey, kFirefoxTypicalExecutable, "Mozilla Firefox",
80 BrowserType::kFirefox},
81 {kOperaVarName, kOperaKey, kOperaTypicalExecutable, "Opera",
82 BrowserType::kOpera},
83 {kSafariVarName, kSafariKey, L"", "Safari", BrowserType::kSafari},
84 {kEdgeVarName, kEdgeKey, kEdgeTypicalExecutable, "Microsoft Edge",
85 BrowserType::kEdge},
86 };
87
88 // DDE Callback function which is not used in our case at all.
DdeCallback(UINT type,UINT format,HCONV handle,HSZ string1,HSZ string2,HDDEDATA data,ULONG_PTR data1,ULONG_PTR data2)89 HDDEDATA CALLBACK DdeCallback(UINT type,
90 UINT format,
91 HCONV handle,
92 HSZ string1,
93 HSZ string2,
94 HDDEDATA data,
95 ULONG_PTR data1,
96 ULONG_PTR data2) {
97 return NULL;
98 }
99
PercentEncodeCommas(std::wstring * url)100 void PercentEncodeCommas(std::wstring* url) {
101 size_t pos = url->find(L",");
102 while (pos != std::wstring::npos) {
103 url->replace(pos, 1, L"%2C");
104 pos = url->find(L",", pos);
105 }
106 }
107
PercentUnencodeQuotes(std::wstring * url)108 void PercentUnencodeQuotes(std::wstring* url) {
109 base::ReplaceSubstringsAfterOffset(url, 0, L"%27", L"'");
110 }
111
GetBrowserLocation(const wchar_t * regkey_name)112 std::wstring GetBrowserLocation(const wchar_t* regkey_name) {
113 DCHECK(regkey_name);
114 base::win::RegKey key;
115 if (ERROR_SUCCESS != key.Open(HKEY_LOCAL_MACHINE, regkey_name, KEY_READ) &&
116 ERROR_SUCCESS != key.Open(HKEY_CURRENT_USER, regkey_name, KEY_READ)) {
117 LOG(ERROR) << "Could not open registry key " << regkey_name
118 << "! Error Code:" << GetLastError();
119 return std::wstring();
120 }
121 std::wstring location;
122 if (ERROR_SUCCESS != key.ReadValue(NULL, &location))
123 return std::wstring();
124 return location;
125 }
126
FindBrowserMapping(base::StringPiece16 path,bool compare_typical_executable)127 const BrowserVarMapping* FindBrowserMapping(base::StringPiece16 path,
128 bool compare_typical_executable) {
129 // If |compare_typical_executable| is true: also look at executable filenames,
130 // to reduce false-negatives when the path is specified explicitly by the
131 // admin.
132 if (path.empty())
133 path = kIEVarName;
134 for (const auto& mapping : kBrowserVarMappings) {
135 if (!path.compare(mapping.var_name) ||
136 (compare_typical_executable && *mapping.typical_executable &&
137 base::EndsWith(path, mapping.typical_executable,
138 base::CompareCase::INSENSITIVE_ASCII))) {
139 return &mapping;
140 }
141 }
142 return nullptr;
143 }
144
ExpandPresetBrowsers(std::wstring * str)145 void ExpandPresetBrowsers(std::wstring* str) {
146 const auto* mapping = FindBrowserMapping(*str, false);
147 if (mapping)
148 *str = GetBrowserLocation(mapping->registry_key);
149 }
150
ExpandUrlVarName(std::wstring * arg,const std::wstring & url_spec)151 bool ExpandUrlVarName(std::wstring* arg, const std::wstring& url_spec) {
152 size_t url_index = arg->find(kUrlVarName);
153 if (url_index == std::wstring::npos)
154 return false;
155 arg->replace(url_index, wcslen(kUrlVarName), url_spec);
156 return true;
157 }
158
ExpandEnvironmentVariables(std::wstring * arg)159 void ExpandEnvironmentVariables(std::wstring* arg) {
160 DWORD expanded_size = 0;
161 expanded_size = ::ExpandEnvironmentStrings(arg->c_str(), NULL, expanded_size);
162 if (expanded_size == 0)
163 return;
164
165 // The expected buffer length as defined in MSDN is chars + null + 1.
166 std::unique_ptr<wchar_t[]> out(new wchar_t[expanded_size + 2]);
167 expanded_size =
168 ::ExpandEnvironmentStrings(arg->c_str(), out.get(), expanded_size);
169 if (expanded_size != 0)
170 *arg = out.get();
171 }
172
AppendCommandLineArguments(base::CommandLine * cmd_line,const std::vector<std::string> & raw_args,const GURL & url)173 void AppendCommandLineArguments(base::CommandLine* cmd_line,
174 const std::vector<std::string>& raw_args,
175 const GURL& url) {
176 std::wstring url_spec = base::UTF8ToWide(url.spec());
177 // IE has some quirks with quote characters. Send them verbatim instead
178 // of percent-encoding them.
179 PercentUnencodeQuotes(&url_spec);
180 std::vector<std::wstring> command_line;
181 bool contains_url = false;
182 for (const auto& arg : raw_args) {
183 std::wstring expanded_arg = base::UTF8ToWide(arg);
184 ExpandEnvironmentVariables(&expanded_arg);
185 if (ExpandUrlVarName(&expanded_arg, url_spec))
186 contains_url = true;
187 cmd_line->AppendArgNative(expanded_arg);
188 }
189 if (!contains_url)
190 cmd_line->AppendArgNative(url_spec);
191 }
192
IsInternetExplorer(base::StringPiece path)193 bool IsInternetExplorer(base::StringPiece path) {
194 // We don't treat IExplore.exe as Internet Explorer here. This way, admins can
195 // set |AlternativeBrowserPath| to IExplore.exe to disable DDE, if it's
196 // causing issues or slowness.
197 return (path.empty() || base::EqualsASCII(kIExploreKey, path));
198 }
199
TryLaunchWithDde(const GURL & url,const std::string & path)200 bool TryLaunchWithDde(const GURL& url, const std::string& path) {
201 base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
202 base::BlockingType::WILL_BLOCK);
203 if (!IsInternetExplorer(path))
204 return false;
205
206 DWORD dde_instance = 0;
207 if (DdeInitialize(&dde_instance, DdeCallback, CBF_FAIL_ALLSVRXACTIONS, 0) !=
208 DMLERR_NO_ERROR) {
209 return false;
210 }
211
212 bool success = false;
213 HCONV openurl_service_instance;
214 HCONV activate_service_instance;
215 {
216 HSZ service =
217 DdeCreateStringHandle(dde_instance, kIExploreDdeHost, CP_WINUNICODE);
218 HSZ openurl_topic =
219 DdeCreateStringHandle(dde_instance, L"WWW_OpenURL", CP_WINUNICODE);
220 HSZ activate_topic =
221 DdeCreateStringHandle(dde_instance, L"WWW_Activate", CP_WINUNICODE);
222 openurl_service_instance =
223 DdeConnect(dde_instance, service, openurl_topic, NULL);
224 activate_service_instance =
225 DdeConnect(dde_instance, service, activate_topic, NULL);
226 DdeFreeStringHandle(dde_instance, service);
227 DdeFreeStringHandle(dde_instance, openurl_topic);
228 DdeFreeStringHandle(dde_instance, activate_topic);
229 }
230
231 if (openurl_service_instance) {
232 // Percent-encode commas and spaces because those mean something else
233 // for the WWW_OpenURL verb and the url is trimmed on the first one.
234 // Spaces are already encoded by GURL.
235 std::wstring encoded_url(base::UTF8ToWide(url.spec()));
236 PercentUnencodeQuotes(&encoded_url);
237 PercentEncodeCommas(&encoded_url);
238
239 success =
240 DdeClientTransaction(
241 reinterpret_cast<LPBYTE>(const_cast<wchar_t*>(encoded_url.data())),
242 encoded_url.size() * sizeof(wchar_t), openurl_service_instance, 0,
243 0, XTYP_EXECUTE, TIMEOUT_ASYNC, NULL) != 0;
244 DdeDisconnect(openurl_service_instance);
245 if (activate_service_instance) {
246 if (success) {
247 // Bring window to the front.
248 wchar_t cmd[] = L"0xFFFFFFFF,0x0";
249 DdeClientTransaction(reinterpret_cast<LPBYTE>(cmd), sizeof(cmd),
250 activate_service_instance, 0, 0, XTYP_EXECUTE,
251 TIMEOUT_ASYNC, NULL);
252 }
253 DdeDisconnect(activate_service_instance);
254 }
255 }
256 DdeUninitialize(dde_instance);
257 return success;
258 }
259
CreateCommandLine(const GURL & url,const std::string & utf8_path,const std::vector<std::string> & params)260 base::CommandLine CreateCommandLine(const GURL& url,
261 const std::string& utf8_path,
262 const std::vector<std::string>& params) {
263 std::wstring path = base::UTF8ToWide(utf8_path);
264 ExpandPresetBrowsers(&path);
265 ExpandEnvironmentVariables(&path);
266 base::CommandLine cmd_line(std::vector<std::wstring>{path});
267
268 AppendCommandLineArguments(&cmd_line, params, url);
269
270 return cmd_line;
271 }
272
TryLaunchWithExec(const GURL & url,const std::string & path,const std::vector<std::string> & args)273 bool TryLaunchWithExec(const GURL& url,
274 const std::string& path,
275 const std::vector<std::string>& args) {
276 base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
277 base::BlockingType::MAY_BLOCK);
278 CHECK(url.SchemeIsHTTPOrHTTPS() || url.SchemeIsFile());
279
280 auto cmd_line = CreateCommandLine(url, path, args);
281
282 base::LaunchOptions options;
283 if (!base::LaunchProcess(cmd_line, options).IsValid()) {
284 LOG(ERROR) << "Could not start the alternative browser! Error: "
285 << GetLastError();
286 return false;
287 }
288 return true;
289 }
290
TryLaunchBlocking(GURL url,std::string path,std::vector<std::string> params,LaunchCallback cb)291 void TryLaunchBlocking(GURL url,
292 std::string path,
293 std::vector<std::string> params,
294 LaunchCallback cb) {
295 const bool success =
296 (TryLaunchWithDde(url, path) || TryLaunchWithExec(url, path, params));
297 content::GetUIThreadTaskRunner({})->PostTask(
298 FROM_HERE,
299 base::BindOnce(
300 [](bool success, LaunchCallback cb) { std::move(cb).Run(success); },
301 success, std::move(cb)));
302 }
303
304 } // namespace
305
306 AlternativeBrowserDriver::~AlternativeBrowserDriver() = default;
307
AlternativeBrowserDriverImpl(const BrowserSwitcherPrefs * prefs)308 AlternativeBrowserDriverImpl::AlternativeBrowserDriverImpl(
309 const BrowserSwitcherPrefs* prefs)
310 : prefs_(prefs) {}
311
312 AlternativeBrowserDriverImpl::~AlternativeBrowserDriverImpl() = default;
313
TryLaunch(const GURL & url,LaunchCallback cb)314 void AlternativeBrowserDriverImpl::TryLaunch(const GURL& url,
315 LaunchCallback cb) {
316 DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
317 VLOG(2) << "Launching alternative browser...";
318 VLOG(2) << " path = " << prefs_->GetAlternativeBrowserPath();
319 VLOG(2) << " url = " << url.spec();
320 base::ThreadPool::PostTask(
321 FROM_HERE,
322 {base::MayBlock(), base::TaskPriority::USER_BLOCKING,
323 base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
324 base::BindOnce(&TryLaunchBlocking, url,
325 prefs_->GetAlternativeBrowserPath(),
326 prefs_->GetAlternativeBrowserParameters(), std::move(cb)));
327 }
328
GetBrowserName() const329 std::string AlternativeBrowserDriverImpl::GetBrowserName() const {
330 std::wstring path = base::UTF8ToWide(prefs_->GetAlternativeBrowserPath());
331 const auto* mapping = FindBrowserMapping(path, false);
332 return mapping ? mapping->browser_name : std::string();
333 }
334
GetBrowserType() const335 BrowserType AlternativeBrowserDriverImpl::GetBrowserType() const {
336 std::wstring path = base::UTF8ToWide(prefs_->GetAlternativeBrowserPath());
337 const auto* mapping = FindBrowserMapping(path, true);
338 return mapping ? mapping->browser_type : BrowserType::kUnknown;
339 }
340
CreateCommandLine(const GURL & url)341 base::CommandLine AlternativeBrowserDriverImpl::CreateCommandLine(
342 const GURL& url) {
343 return browser_switcher::CreateCommandLine(
344 url, prefs_->GetAlternativeBrowserPath(),
345 prefs_->GetAlternativeBrowserParameters());
346 }
347
348 } // namespace browser_switcher
349