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