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