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 "content/browser/webui/url_data_manager_backend.h" 6 7 #include <set> 8 9 #include "base/basictypes.h" 10 #include "base/bind.h" 11 #include "base/command_line.h" 12 #include "base/compiler_specific.h" 13 #include "base/debug/trace_event.h" 14 #include "base/lazy_instance.h" 15 #include "base/memory/ref_counted.h" 16 #include "base/memory/ref_counted_memory.h" 17 #include "base/memory/weak_ptr.h" 18 #include "base/message_loop/message_loop.h" 19 #include "base/strings/string_util.h" 20 #include "content/browser/fileapi/chrome_blob_storage_context.h" 21 #include "content/browser/histogram_internals_request_job.h" 22 #include "content/browser/net/view_blob_internals_job_factory.h" 23 #include "content/browser/net/view_http_cache_job_factory.h" 24 #include "content/browser/resource_context_impl.h" 25 #include "content/browser/tcmalloc_internals_request_job.h" 26 #include "content/browser/webui/shared_resources_data_source.h" 27 #include "content/browser/webui/url_data_source_impl.h" 28 #include "content/public/browser/browser_thread.h" 29 #include "content/public/browser/content_browser_client.h" 30 #include "content/public/browser/render_process_host.h" 31 #include "content/public/browser/resource_request_info.h" 32 #include "content/public/common/url_constants.h" 33 #include "net/base/io_buffer.h" 34 #include "net/base/net_errors.h" 35 #include "net/http/http_response_headers.h" 36 #include "net/http/http_status_code.h" 37 #include "net/url_request/url_request.h" 38 #include "net/url_request/url_request_context.h" 39 #include "net/url_request/url_request_job.h" 40 #include "net/url_request/url_request_job_factory.h" 41 #include "url/url_util.h" 42 #include "webkit/browser/appcache/view_appcache_internals_job.h" 43 44 using appcache::AppCacheService; 45 46 namespace content { 47 48 namespace { 49 50 // TODO(tsepez) remove unsafe-eval when bidichecker_packaged.js fixed. 51 const char kChromeURLContentSecurityPolicyHeaderBase[] = 52 "Content-Security-Policy: script-src chrome://resources " 53 "'self' 'unsafe-eval'; "; 54 55 const char kChromeURLXFrameOptionsHeader[] = "X-Frame-Options: DENY"; 56 57 bool SchemeIsInSchemes(const std::string& scheme, 58 const std::vector<std::string>& schemes) { 59 return std::find(schemes.begin(), schemes.end(), scheme) != schemes.end(); 60 } 61 62 // Parse a URL into the components used to resolve its request. |source_name| 63 // is the hostname and |path| is the remaining portion of the URL. 64 void URLToRequest(const GURL& url, std::string* source_name, 65 std::string* path) { 66 std::vector<std::string> additional_schemes; 67 DCHECK(url.SchemeIs(chrome::kChromeDevToolsScheme) || 68 url.SchemeIs(chrome::kChromeUIScheme) || 69 (GetContentClient()->browser()->GetAdditionalWebUISchemes( 70 &additional_schemes), 71 SchemeIsInSchemes(url.scheme(), additional_schemes))); 72 73 if (!url.is_valid()) { 74 NOTREACHED(); 75 return; 76 } 77 78 // Our input looks like: chrome://source_name/extra_bits?foo . 79 // So the url's "host" is our source, and everything after the host is 80 // the path. 81 source_name->assign(url.host()); 82 83 const std::string& spec = url.possibly_invalid_spec(); 84 const url_parse::Parsed& parsed = url.parsed_for_possibly_invalid_spec(); 85 // + 1 to skip the slash at the beginning of the path. 86 int offset = parsed.CountCharactersBefore(url_parse::Parsed::PATH, false) + 1; 87 88 if (offset < static_cast<int>(spec.size())) 89 path->assign(spec.substr(offset)); 90 } 91 92 } // namespace 93 94 // URLRequestChromeJob is a net::URLRequestJob that manages running 95 // chrome-internal resource requests asynchronously. 96 // It hands off URL requests to ChromeURLDataManager, which asynchronously 97 // calls back once the data is available. 98 class URLRequestChromeJob : public net::URLRequestJob, 99 public base::SupportsWeakPtr<URLRequestChromeJob> { 100 public: 101 // |is_incognito| set when job is generated from an incognito profile. 102 URLRequestChromeJob(net::URLRequest* request, 103 net::NetworkDelegate* network_delegate, 104 URLDataManagerBackend* backend, 105 bool is_incognito); 106 107 // net::URLRequestJob implementation. 108 virtual void Start() OVERRIDE; 109 virtual void Kill() OVERRIDE; 110 virtual bool ReadRawData(net::IOBuffer* buf, 111 int buf_size, 112 int* bytes_read) OVERRIDE; 113 virtual bool GetMimeType(std::string* mime_type) const OVERRIDE; 114 virtual int GetResponseCode() const OVERRIDE; 115 virtual void GetResponseInfo(net::HttpResponseInfo* info) OVERRIDE; 116 117 // Used to notify that the requested data's |mime_type| is ready. 118 void MimeTypeAvailable(const std::string& mime_type); 119 120 // Called by ChromeURLDataManager to notify us that the data blob is ready 121 // for us. 122 void DataAvailable(base::RefCountedMemory* bytes); 123 124 void set_mime_type(const std::string& mime_type) { 125 mime_type_ = mime_type; 126 } 127 128 void set_allow_caching(bool allow_caching) { 129 allow_caching_ = allow_caching; 130 } 131 132 void set_add_content_security_policy(bool add_content_security_policy) { 133 add_content_security_policy_ = add_content_security_policy; 134 } 135 136 void set_content_security_policy_object_source( 137 const std::string& data) { 138 content_security_policy_object_source_ = data; 139 } 140 141 void set_content_security_policy_frame_source( 142 const std::string& data) { 143 content_security_policy_frame_source_ = data; 144 } 145 146 void set_deny_xframe_options(bool deny_xframe_options) { 147 deny_xframe_options_ = deny_xframe_options; 148 } 149 150 // Returns true when job was generated from an incognito profile. 151 bool is_incognito() const { 152 return is_incognito_; 153 } 154 155 private: 156 virtual ~URLRequestChromeJob(); 157 158 // Helper for Start(), to let us start asynchronously. 159 // (This pattern is shared by most net::URLRequestJob implementations.) 160 void StartAsync(); 161 162 // Do the actual copy from data_ (the data we're serving) into |buf|. 163 // Separate from ReadRawData so we can handle async I/O. 164 void CompleteRead(net::IOBuffer* buf, int buf_size, int* bytes_read); 165 166 // The actual data we're serving. NULL until it's been fetched. 167 scoped_refptr<base::RefCountedMemory> data_; 168 // The current offset into the data that we're handing off to our 169 // callers via the Read interfaces. 170 int data_offset_; 171 172 // For async reads, we keep around a pointer to the buffer that 173 // we're reading into. 174 scoped_refptr<net::IOBuffer> pending_buf_; 175 int pending_buf_size_; 176 std::string mime_type_; 177 178 // If true, set a header in the response to prevent it from being cached. 179 bool allow_caching_; 180 181 // If true, set the Content Security Policy (CSP) header. 182 bool add_content_security_policy_; 183 184 // These are used with the CSP. 185 std::string content_security_policy_object_source_; 186 std::string content_security_policy_frame_source_; 187 188 // If true, sets the "X-Frame-Options: DENY" header. 189 bool deny_xframe_options_; 190 191 // True when job is generated from an incognito profile. 192 const bool is_incognito_; 193 194 // The backend is owned by ChromeURLRequestContext and always outlives us. 195 URLDataManagerBackend* backend_; 196 197 base::WeakPtrFactory<URLRequestChromeJob> weak_factory_; 198 199 DISALLOW_COPY_AND_ASSIGN(URLRequestChromeJob); 200 }; 201 202 URLRequestChromeJob::URLRequestChromeJob(net::URLRequest* request, 203 net::NetworkDelegate* network_delegate, 204 URLDataManagerBackend* backend, 205 bool is_incognito) 206 : net::URLRequestJob(request, network_delegate), 207 data_offset_(0), 208 pending_buf_size_(0), 209 allow_caching_(true), 210 add_content_security_policy_(true), 211 content_security_policy_object_source_("object-src 'none';"), 212 content_security_policy_frame_source_("frame-src 'none';"), 213 deny_xframe_options_(true), 214 is_incognito_(is_incognito), 215 backend_(backend), 216 weak_factory_(this) { 217 DCHECK(backend); 218 } 219 220 URLRequestChromeJob::~URLRequestChromeJob() { 221 CHECK(!backend_->HasPendingJob(this)); 222 } 223 224 void URLRequestChromeJob::Start() { 225 // Start reading asynchronously so that all error reporting and data 226 // callbacks happen as they would for network requests. 227 base::MessageLoop::current()->PostTask( 228 FROM_HERE, 229 base::Bind(&URLRequestChromeJob::StartAsync, weak_factory_.GetWeakPtr())); 230 231 TRACE_EVENT_ASYNC_BEGIN1("browser", "DataManager:Request", this, "URL", 232 request_->url().possibly_invalid_spec()); 233 } 234 235 void URLRequestChromeJob::Kill() { 236 backend_->RemoveRequest(this); 237 } 238 239 bool URLRequestChromeJob::GetMimeType(std::string* mime_type) const { 240 *mime_type = mime_type_; 241 return !mime_type_.empty(); 242 } 243 244 int URLRequestChromeJob::GetResponseCode() const { 245 return net::HTTP_OK; 246 } 247 248 void URLRequestChromeJob::GetResponseInfo(net::HttpResponseInfo* info) { 249 DCHECK(!info->headers.get()); 250 // Set the headers so that requests serviced by ChromeURLDataManager return a 251 // status code of 200. Without this they return a 0, which makes the status 252 // indistiguishable from other error types. Instant relies on getting a 200. 253 info->headers = new net::HttpResponseHeaders("HTTP/1.1 200 OK"); 254 255 // Determine the least-privileged content security policy header, if any, 256 // that is compatible with a given WebUI URL, and append it to the existing 257 // response headers. 258 if (add_content_security_policy_) { 259 std::string base = kChromeURLContentSecurityPolicyHeaderBase; 260 base.append(content_security_policy_object_source_); 261 base.append(content_security_policy_frame_source_); 262 info->headers->AddHeader(base); 263 } 264 265 if (deny_xframe_options_) 266 info->headers->AddHeader(kChromeURLXFrameOptionsHeader); 267 268 if (!allow_caching_) 269 info->headers->AddHeader("Cache-Control: no-cache"); 270 } 271 272 void URLRequestChromeJob::MimeTypeAvailable(const std::string& mime_type) { 273 set_mime_type(mime_type); 274 NotifyHeadersComplete(); 275 } 276 277 void URLRequestChromeJob::DataAvailable(base::RefCountedMemory* bytes) { 278 TRACE_EVENT_ASYNC_END0("browser", "DataManager:Request", this); 279 if (bytes) { 280 // The request completed, and we have all the data. 281 // Clear any IO pending status. 282 SetStatus(net::URLRequestStatus()); 283 284 data_ = bytes; 285 int bytes_read; 286 if (pending_buf_.get()) { 287 CHECK(pending_buf_->data()); 288 CompleteRead(pending_buf_.get(), pending_buf_size_, &bytes_read); 289 pending_buf_ = NULL; 290 NotifyReadComplete(bytes_read); 291 } 292 } else { 293 // The request failed. 294 NotifyDone(net::URLRequestStatus(net::URLRequestStatus::FAILED, 295 net::ERR_FAILED)); 296 } 297 } 298 299 bool URLRequestChromeJob::ReadRawData(net::IOBuffer* buf, int buf_size, 300 int* bytes_read) { 301 if (!data_.get()) { 302 SetStatus(net::URLRequestStatus(net::URLRequestStatus::IO_PENDING, 0)); 303 DCHECK(!pending_buf_.get()); 304 CHECK(buf->data()); 305 pending_buf_ = buf; 306 pending_buf_size_ = buf_size; 307 return false; // Tell the caller we're still waiting for data. 308 } 309 310 // Otherwise, the data is available. 311 CompleteRead(buf, buf_size, bytes_read); 312 return true; 313 } 314 315 void URLRequestChromeJob::CompleteRead(net::IOBuffer* buf, int buf_size, 316 int* bytes_read) { 317 int remaining = static_cast<int>(data_->size()) - data_offset_; 318 if (buf_size > remaining) 319 buf_size = remaining; 320 if (buf_size > 0) { 321 memcpy(buf->data(), data_->front() + data_offset_, buf_size); 322 data_offset_ += buf_size; 323 } 324 *bytes_read = buf_size; 325 } 326 327 void URLRequestChromeJob::StartAsync() { 328 if (!request_) 329 return; 330 331 if (!backend_->StartRequest(request_, this)) { 332 NotifyStartError(net::URLRequestStatus(net::URLRequestStatus::FAILED, 333 net::ERR_INVALID_URL)); 334 } 335 } 336 337 namespace { 338 339 // Gets mime type for data that is available from |source| by |path|. 340 // After that, notifies |job| that mime type is available. This method 341 // should be called on the UI thread, but notification is performed on 342 // the IO thread. 343 void GetMimeTypeOnUI(URLDataSourceImpl* source, 344 const std::string& path, 345 const base::WeakPtr<URLRequestChromeJob>& job) { 346 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); 347 std::string mime_type = source->source()->GetMimeType(path); 348 BrowserThread::PostTask( 349 BrowserThread::IO, FROM_HERE, 350 base::Bind(&URLRequestChromeJob::MimeTypeAvailable, job, mime_type)); 351 } 352 353 } // namespace 354 355 namespace { 356 357 class ChromeProtocolHandler 358 : public net::URLRequestJobFactory::ProtocolHandler { 359 public: 360 // |is_incognito| should be set for incognito profiles. 361 ChromeProtocolHandler(ResourceContext* resource_context, 362 bool is_incognito, 363 AppCacheService* appcache_service, 364 ChromeBlobStorageContext* blob_storage_context) 365 : resource_context_(resource_context), 366 is_incognito_(is_incognito), 367 appcache_service_(appcache_service), 368 blob_storage_context_(blob_storage_context) {} 369 virtual ~ChromeProtocolHandler() {} 370 371 virtual net::URLRequestJob* MaybeCreateJob( 372 net::URLRequest* request, 373 net::NetworkDelegate* network_delegate) const OVERRIDE { 374 DCHECK(request); 375 376 // Check for chrome://view-http-cache/*, which uses its own job type. 377 if (ViewHttpCacheJobFactory::IsSupportedURL(request->url())) 378 return ViewHttpCacheJobFactory::CreateJobForRequest(request, 379 network_delegate); 380 381 // Next check for chrome://appcache-internals/, which uses its own job type. 382 if (request->url().SchemeIs(chrome::kChromeUIScheme) && 383 request->url().host() == kChromeUIAppCacheInternalsHost) { 384 return appcache::ViewAppCacheInternalsJobFactory::CreateJobForRequest( 385 request, network_delegate, appcache_service_); 386 } 387 388 // Next check for chrome://blob-internals/, which uses its own job type. 389 if (ViewBlobInternalsJobFactory::IsSupportedURL(request->url())) { 390 return ViewBlobInternalsJobFactory::CreateJobForRequest( 391 request, network_delegate, blob_storage_context_->controller()); 392 } 393 394 #if defined(USE_TCMALLOC) 395 // Next check for chrome://tcmalloc/, which uses its own job type. 396 if (request->url().SchemeIs(chrome::kChromeUIScheme) && 397 request->url().host() == kChromeUITcmallocHost) { 398 return new TcmallocInternalsRequestJob(request, network_delegate); 399 } 400 #endif 401 402 // Next check for chrome://histograms/, which uses its own job type. 403 if (request->url().SchemeIs(chrome::kChromeUIScheme) && 404 request->url().host() == kChromeUIHistogramHost) { 405 return new HistogramInternalsRequestJob(request, network_delegate); 406 } 407 408 // Fall back to using a custom handler 409 return new URLRequestChromeJob( 410 request, network_delegate, 411 GetURLDataManagerForResourceContext(resource_context_), is_incognito_); 412 } 413 414 virtual bool IsSafeRedirectTarget(const GURL& location) const OVERRIDE { 415 return false; 416 } 417 418 private: 419 // These members are owned by ProfileIOData, which owns this ProtocolHandler. 420 content::ResourceContext* const resource_context_; 421 422 // True when generated from an incognito profile. 423 const bool is_incognito_; 424 AppCacheService* appcache_service_; 425 ChromeBlobStorageContext* blob_storage_context_; 426 427 DISALLOW_COPY_AND_ASSIGN(ChromeProtocolHandler); 428 }; 429 430 } // namespace 431 432 URLDataManagerBackend::URLDataManagerBackend() 433 : next_request_id_(0) { 434 URLDataSource* shared_source = new SharedResourcesDataSource(); 435 URLDataSourceImpl* source_impl = 436 new URLDataSourceImpl(shared_source->GetSource(), shared_source); 437 AddDataSource(source_impl); 438 } 439 440 URLDataManagerBackend::~URLDataManagerBackend() { 441 for (DataSourceMap::iterator i = data_sources_.begin(); 442 i != data_sources_.end(); ++i) { 443 i->second->backend_ = NULL; 444 } 445 data_sources_.clear(); 446 } 447 448 // static 449 net::URLRequestJobFactory::ProtocolHandler* 450 URLDataManagerBackend::CreateProtocolHandler( 451 content::ResourceContext* resource_context, 452 bool is_incognito, 453 AppCacheService* appcache_service, 454 ChromeBlobStorageContext* blob_storage_context) { 455 DCHECK(resource_context); 456 return new ChromeProtocolHandler( 457 resource_context, is_incognito, appcache_service, blob_storage_context); 458 } 459 460 void URLDataManagerBackend::AddDataSource( 461 URLDataSourceImpl* source) { 462 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 463 DataSourceMap::iterator i = data_sources_.find(source->source_name()); 464 if (i != data_sources_.end()) { 465 if (!source->source()->ShouldReplaceExistingSource()) 466 return; 467 i->second->backend_ = NULL; 468 } 469 data_sources_[source->source_name()] = source; 470 source->backend_ = this; 471 } 472 473 bool URLDataManagerBackend::HasPendingJob( 474 URLRequestChromeJob* job) const { 475 for (PendingRequestMap::const_iterator i = pending_requests_.begin(); 476 i != pending_requests_.end(); ++i) { 477 if (i->second == job) 478 return true; 479 } 480 return false; 481 } 482 483 bool URLDataManagerBackend::StartRequest(const net::URLRequest* request, 484 URLRequestChromeJob* job) { 485 // Parse the URL into a request for a source and path. 486 std::string source_name; 487 std::string path; 488 URLToRequest(request->url(), &source_name, &path); 489 490 // Look up the data source for the request. 491 DataSourceMap::iterator i = data_sources_.find(source_name); 492 if (i == data_sources_.end()) 493 return false; 494 495 URLDataSourceImpl* source = i->second.get(); 496 497 if (!source->source()->ShouldServiceRequest(request)) 498 return false; 499 source->source()->WillServiceRequest(request, &path); 500 501 // Save this request so we know where to send the data. 502 RequestID request_id = next_request_id_++; 503 pending_requests_.insert(std::make_pair(request_id, job)); 504 505 job->set_allow_caching(source->source()->AllowCaching()); 506 job->set_add_content_security_policy( 507 source->source()->ShouldAddContentSecurityPolicy()); 508 job->set_content_security_policy_object_source( 509 source->source()->GetContentSecurityPolicyObjectSrc()); 510 job->set_content_security_policy_frame_source( 511 source->source()->GetContentSecurityPolicyFrameSrc()); 512 job->set_deny_xframe_options( 513 source->source()->ShouldDenyXFrameOptions()); 514 515 // Look up additional request info to pass down. 516 int render_process_id = -1; 517 int render_view_id = -1; 518 ResourceRequestInfo::GetRenderViewForRequest(request, 519 &render_process_id, 520 &render_view_id); 521 522 // Forward along the request to the data source. 523 base::MessageLoop* target_message_loop = 524 source->source()->MessageLoopForRequestPath(path); 525 if (!target_message_loop) { 526 job->MimeTypeAvailable(source->source()->GetMimeType(path)); 527 // Eliminate potentially dangling pointer to avoid future use. 528 job = NULL; 529 530 // The DataSource is agnostic to which thread StartDataRequest is called 531 // on for this path. Call directly into it from this thread, the IO 532 // thread. 533 source->source()->StartDataRequest( 534 path, render_process_id, render_view_id, 535 base::Bind(&URLDataSourceImpl::SendResponse, source, request_id)); 536 } else { 537 // URLRequestChromeJob should receive mime type before data. This 538 // is guaranteed because request for mime type is placed in the 539 // message loop before request for data. And correspondingly their 540 // replies are put on the IO thread in the same order. 541 target_message_loop->PostTask( 542 FROM_HERE, 543 base::Bind(&GetMimeTypeOnUI, 544 scoped_refptr<URLDataSourceImpl>(source), 545 path, job->AsWeakPtr())); 546 547 // The DataSource wants StartDataRequest to be called on a specific thread, 548 // usually the UI thread, for this path. 549 target_message_loop->PostTask( 550 FROM_HERE, 551 base::Bind(&URLDataManagerBackend::CallStartRequest, 552 make_scoped_refptr(source), path, render_process_id, 553 render_view_id, request_id)); 554 } 555 return true; 556 } 557 558 void URLDataManagerBackend::CallStartRequest( 559 scoped_refptr<URLDataSourceImpl> source, 560 const std::string& path, 561 int render_process_id, 562 int render_view_id, 563 int request_id) { 564 if (BrowserThread::CurrentlyOn(BrowserThread::UI) && 565 render_process_id != -1 && 566 !RenderProcessHost::FromID(render_process_id)) { 567 // Make the request fail if its initiating renderer is no longer valid. 568 // This can happen when the IO thread posts this task just before the 569 // renderer shuts down. 570 source->SendResponse(request_id, NULL); 571 return; 572 } 573 source->source()->StartDataRequest( 574 path, 575 render_process_id, 576 render_view_id, 577 base::Bind(&URLDataSourceImpl::SendResponse, source, request_id)); 578 } 579 580 void URLDataManagerBackend::RemoveRequest(URLRequestChromeJob* job) { 581 // Remove the request from our list of pending requests. 582 // If/when the source sends the data that was requested, the data will just 583 // be thrown away. 584 for (PendingRequestMap::iterator i = pending_requests_.begin(); 585 i != pending_requests_.end(); ++i) { 586 if (i->second == job) { 587 pending_requests_.erase(i); 588 return; 589 } 590 } 591 } 592 593 void URLDataManagerBackend::DataAvailable(RequestID request_id, 594 base::RefCountedMemory* bytes) { 595 // Forward this data on to the pending net::URLRequest, if it exists. 596 PendingRequestMap::iterator i = pending_requests_.find(request_id); 597 if (i != pending_requests_.end()) { 598 URLRequestChromeJob* job(i->second); 599 pending_requests_.erase(i); 600 job->DataAvailable(bytes); 601 } 602 } 603 604 namespace { 605 606 class DevToolsJobFactory 607 : public net::URLRequestJobFactory::ProtocolHandler { 608 public: 609 // |is_incognito| should be set for incognito profiles. 610 DevToolsJobFactory(content::ResourceContext* resource_context, 611 bool is_incognito); 612 virtual ~DevToolsJobFactory(); 613 614 virtual net::URLRequestJob* MaybeCreateJob( 615 net::URLRequest* request, 616 net::NetworkDelegate* network_delegate) const OVERRIDE; 617 618 private: 619 // |resource_context_| and |network_delegate_| are owned by ProfileIOData, 620 // which owns this ProtocolHandler. 621 content::ResourceContext* const resource_context_; 622 623 // True when generated from an incognito profile. 624 const bool is_incognito_; 625 626 DISALLOW_COPY_AND_ASSIGN(DevToolsJobFactory); 627 }; 628 629 DevToolsJobFactory::DevToolsJobFactory( 630 content::ResourceContext* resource_context, 631 bool is_incognito) 632 : resource_context_(resource_context), 633 is_incognito_(is_incognito) { 634 DCHECK(resource_context_); 635 } 636 637 DevToolsJobFactory::~DevToolsJobFactory() {} 638 639 net::URLRequestJob* 640 DevToolsJobFactory::MaybeCreateJob( 641 net::URLRequest* request, net::NetworkDelegate* network_delegate) const { 642 return new URLRequestChromeJob( 643 request, network_delegate, 644 GetURLDataManagerForResourceContext(resource_context_), is_incognito_); 645 } 646 647 } // namespace 648 649 net::URLRequestJobFactory::ProtocolHandler* 650 CreateDevToolsProtocolHandler(content::ResourceContext* resource_context, 651 bool is_incognito) { 652 return new DevToolsJobFactory(resource_context, is_incognito); 653 } 654 655 } // namespace content 656