external_protocol_handler.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
1// Copyright (c) 2012 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/external_protocol/external_protocol_handler.h"
6
7#include <set>
8
9#include "base/bind.h"
10#include "base/logging.h"
11#include "base/message_loop.h"
12#include "base/prefs/pref_registry_simple.h"
13#include "base/prefs/pref_service.h"
14#include "base/string_util.h"
15#include "base/threading/thread.h"
16#include "build/build_config.h"
17#include "chrome/browser/browser_process.h"
18#include "chrome/browser/platform_util.h"
19#include "chrome/browser/prefs/scoped_user_pref_update.h"
20#include "chrome/common/pref_names.h"
21#include "content/public/browser/browser_thread.h"
22#include "googleurl/src/gurl.h"
23#include "net/base/escape.h"
24
25using content::BrowserThread;
26
27// Whether we accept requests for launching external protocols. This is set to
28// false every time an external protocol is requested, and set back to true on
29// each user gesture. This variable should only be accessed from the UI thread.
30static bool g_accept_requests = true;
31
32namespace {
33
34// Functions enabling unit testing. Using a NULL delegate will use the default
35// behavior; if a delegate is provided it will be used instead.
36ShellIntegration::DefaultProtocolClientWorker* CreateShellWorker(
37    ShellIntegration::DefaultWebClientObserver* observer,
38    const std::string& protocol,
39    ExternalProtocolHandler::Delegate* delegate) {
40  if (!delegate)
41    return new ShellIntegration::DefaultProtocolClientWorker(observer,
42                                                             protocol);
43
44  return delegate->CreateShellWorker(observer, protocol);
45}
46
47ExternalProtocolHandler::BlockState GetBlockStateWithDelegate(
48    const std::string& scheme,
49    ExternalProtocolHandler::Delegate* delegate) {
50  if (!delegate)
51    return ExternalProtocolHandler::GetBlockState(scheme);
52
53  return delegate->GetBlockState(scheme);
54}
55
56void RunExternalProtocolDialogWithDelegate(
57    const GURL& url,
58    int render_process_host_id,
59    int routing_id,
60    ExternalProtocolHandler::Delegate* delegate) {
61  if (!delegate) {
62    ExternalProtocolHandler::RunExternalProtocolDialog(url,
63                                                       render_process_host_id,
64                                                       routing_id);
65  } else {
66    delegate->RunExternalProtocolDialog(url, render_process_host_id,
67                                        routing_id);
68  }
69}
70
71void LaunchUrlWithoutSecurityCheckWithDelegate(
72    const GURL& url,
73    ExternalProtocolHandler::Delegate* delegate) {
74  if (!delegate)
75    ExternalProtocolHandler::LaunchUrlWithoutSecurityCheck(url);
76  else
77    delegate->LaunchUrlWithoutSecurityCheck(url);
78}
79
80// When we are about to launch a URL with the default OS level application,
81// we check if that external application will be us. If it is we just ignore
82// the request.
83class ExternalDefaultProtocolObserver
84    : public ShellIntegration::DefaultWebClientObserver {
85 public:
86  ExternalDefaultProtocolObserver(const GURL& escaped_url,
87                                  int render_process_host_id,
88                                  int tab_contents_id,
89                                  bool prompt_user,
90                                  ExternalProtocolHandler::Delegate* delegate)
91      : delegate_(delegate),
92        escaped_url_(escaped_url),
93        render_process_host_id_(render_process_host_id),
94        tab_contents_id_(tab_contents_id),
95        prompt_user_(prompt_user) {}
96
97  virtual void SetDefaultWebClientUIState(
98      ShellIntegration::DefaultWebClientUIState state) OVERRIDE {
99    DCHECK_EQ(MessageLoop::TYPE_UI, MessageLoop::current()->type());
100
101    // If we are still working out if we're the default, or we've found
102    // out we definately are the default, we end here.
103    if (state == ShellIntegration::STATE_PROCESSING) {
104      return;
105    }
106
107    if (delegate_)
108      delegate_->FinishedProcessingCheck();
109
110    if (state == ShellIntegration::STATE_IS_DEFAULT) {
111      if (delegate_)
112        delegate_->BlockRequest();
113      return;
114    }
115
116    // If we get here, either we are not the default or we cannot work out
117    // what the default is, so we proceed.
118    if (prompt_user_) {
119      // Ask the user if they want to allow the protocol. This will call
120      // LaunchUrlWithoutSecurityCheck if the user decides to accept the
121      // protocol.
122      RunExternalProtocolDialogWithDelegate(escaped_url_,
123          render_process_host_id_, tab_contents_id_, delegate_);
124      return;
125    }
126
127    LaunchUrlWithoutSecurityCheckWithDelegate(escaped_url_, delegate_);
128  }
129
130  virtual bool IsOwnedByWorker() OVERRIDE { return true; }
131
132 private:
133  ExternalProtocolHandler::Delegate* delegate_;
134  GURL escaped_url_;
135  int render_process_host_id_;
136  int tab_contents_id_;
137  bool prompt_user_;
138};
139
140}  // namespace
141
142// static
143void ExternalProtocolHandler::PrepopulateDictionary(DictionaryValue* win_pref) {
144  static bool is_warm = false;
145  if (is_warm)
146    return;
147  is_warm = true;
148
149  static const char* const denied_schemes[] = {
150    "afp",
151    "data",
152    "disk",
153    "disks",
154    // ShellExecuting file:///C:/WINDOWS/system32/notepad.exe will simply
155    // execute the file specified!  Hopefully we won't see any "file" schemes
156    // because we think of file:// URLs as handled URLs, but better to be safe
157    // than to let an attacker format the user's hard drive.
158    "file",
159    "hcp",
160    "javascript",
161    "ms-help",
162    "nntp",
163    "shell",
164    "vbscript",
165    // view-source is a special case in chrome. When it comes through an
166    // iframe or a redirect, it looks like an external protocol, but we don't
167    // want to shellexecute it.
168    "view-source",
169    "vnd.ms.radio",
170  };
171
172  static const char* const allowed_schemes[] = {
173    "mailto",
174    "news",
175    "snews",
176#if defined(OS_WIN)
177    "ms-windows-store",
178#endif
179  };
180
181  bool should_block;
182  for (size_t i = 0; i < arraysize(denied_schemes); ++i) {
183    if (!win_pref->GetBoolean(denied_schemes[i], &should_block)) {
184      win_pref->SetBoolean(denied_schemes[i], true);
185    }
186  }
187
188  for (size_t i = 0; i < arraysize(allowed_schemes); ++i) {
189    if (!win_pref->GetBoolean(allowed_schemes[i], &should_block)) {
190      win_pref->SetBoolean(allowed_schemes[i], false);
191    }
192  }
193}
194
195// static
196ExternalProtocolHandler::BlockState ExternalProtocolHandler::GetBlockState(
197    const std::string& scheme) {
198  // If we are being carpet bombed, block the request.
199  if (!g_accept_requests)
200    return BLOCK;
201
202  if (scheme.length() == 1) {
203    // We have a URL that looks something like:
204    //   C:/WINDOWS/system32/notepad.exe
205    // ShellExecuting this URL will cause the specified program to be executed.
206    return BLOCK;
207  }
208
209  // Check the stored prefs.
210  // TODO(pkasting): http://b/1119651 This kind of thing should go in the
211  // preferences on the profile, not in the local state.
212  PrefService* pref = g_browser_process->local_state();
213  if (pref) {  // May be NULL during testing.
214    DictionaryPrefUpdate update_excluded_schemas(pref, prefs::kExcludedSchemes);
215
216    // Warm up the dictionary if needed.
217    PrepopulateDictionary(update_excluded_schemas.Get());
218
219    bool should_block;
220    if (update_excluded_schemas->GetBoolean(scheme, &should_block))
221      return should_block ? BLOCK : DONT_BLOCK;
222  }
223
224  return UNKNOWN;
225}
226
227// static
228void ExternalProtocolHandler::SetBlockState(const std::string& scheme,
229                                            BlockState state) {
230  // Set in the stored prefs.
231  // TODO(pkasting): http://b/1119651 This kind of thing should go in the
232  // preferences on the profile, not in the local state.
233  PrefService* pref = g_browser_process->local_state();
234  if (pref) {  // May be NULL during testing.
235    DictionaryPrefUpdate update_excluded_schemas(pref, prefs::kExcludedSchemes);
236
237    if (state == UNKNOWN) {
238      update_excluded_schemas->Remove(scheme, NULL);
239    } else {
240      update_excluded_schemas->SetBoolean(scheme, (state == BLOCK));
241    }
242  }
243}
244
245// static
246void ExternalProtocolHandler::LaunchUrlWithDelegate(const GURL& url,
247                                                    int render_process_host_id,
248                                                    int tab_contents_id,
249                                                    Delegate* delegate) {
250  DCHECK_EQ(MessageLoop::TYPE_UI, MessageLoop::current()->type());
251
252  // Escape the input scheme to be sure that the command does not
253  // have parameters unexpected by the external program.
254  std::string escaped_url_string = net::EscapeExternalHandlerValue(url.spec());
255  GURL escaped_url(escaped_url_string);
256  BlockState block_state = GetBlockStateWithDelegate(escaped_url.scheme(),
257                                                     delegate);
258  if (block_state == BLOCK) {
259    if (delegate)
260      delegate->BlockRequest();
261    return;
262  }
263
264  g_accept_requests = false;
265
266  // The worker creates tasks with references to itself and puts them into
267  // message loops. When no tasks are left it will delete the observer and
268  // eventually be deleted itself.
269  ShellIntegration::DefaultWebClientObserver* observer =
270      new ExternalDefaultProtocolObserver(url,
271                                          render_process_host_id,
272                                          tab_contents_id,
273                                          block_state == UNKNOWN,
274                                          delegate);
275  scoped_refptr<ShellIntegration::DefaultProtocolClientWorker> worker =
276      CreateShellWorker(observer, escaped_url.scheme(), delegate);
277
278  // Start the check process running. This will send tasks to the FILE thread
279  // and when the answer is known will send the result back to the observer on
280  // the UI thread.
281  worker->StartCheckIsDefault();
282}
283
284// static
285void ExternalProtocolHandler::LaunchUrlWithoutSecurityCheck(const GURL& url) {
286#if defined(OS_MACOSX)
287  // This must run on the UI thread on OS X.
288  platform_util::OpenExternal(url);
289#else
290  // Otherwise put this work on the file thread. On Windows ShellExecute may
291  // block for a significant amount of time, and it shouldn't hurt on Linux.
292  BrowserThread::PostTask(
293      BrowserThread::FILE,
294      FROM_HERE,
295      base::Bind(&platform_util::OpenExternal, url));
296#endif
297}
298
299// static
300void ExternalProtocolHandler::RegisterPrefs(PrefRegistrySimple* registry) {
301  registry->RegisterDictionaryPref(prefs::kExcludedSchemes);
302}
303
304// static
305void ExternalProtocolHandler::PermitLaunchUrl() {
306  DCHECK_EQ(MessageLoop::TYPE_UI, MessageLoop::current()->type());
307  g_accept_requests = true;
308}
309