external_protocol_handler.cc revision ca12bfac764ba476d6cd062bf1dde12cc64c3f40
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/message_loop.h" 12#include "base/prefs/pref_registry_simple.h" 13#include "base/prefs/pref_service.h" 14#include "base/strings/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 "net/base/escape.h" 23#include "url/gurl.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(base::MessageLoop::TYPE_UI, base::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(base::MessageLoop::TYPE_UI, base::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(base::MessageLoop::TYPE_UI, base::MessageLoop::current()->type()); 307 g_accept_requests = true; 308} 309