// Copyright (c) 2012 The Chromium Embedded Framework Authors. // Portions copyright (c) 2006-2009 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "libcef/browser/net/resource_request_job.h" #include #include #include "include/cef_callback.h" #include "libcef/browser/cookie_manager_impl.h" #include "libcef/browser/thread_util.h" #include "libcef/common/request_impl.h" #include "libcef/common/response_impl.h" #include "base/logging.h" #include "net/base/io_buffer.h" #include "net/base/load_flags.h" #include "net/base/mime_util.h" #include "net/http/http_response_headers.h" #include "net/url_request/http_user_agent_settings.h" #include "net/url_request/url_request.h" #include "net/url_request/url_request_context.h" using net::URLRequestStatus; namespace { bool SetHeaderIfMissing(CefRequest::HeaderMap& headerMap, const std::string& name, const std::string& value) { if (value.empty()) return false; CefRequest::HeaderMap::const_iterator it = headerMap.find(name); if (it == headerMap.end()) { headerMap.insert(std::make_pair(name, value)); return true; } return false; } } // namespace // Client callback for asynchronous response continuation. class CefResourceRequestJobCallback : public CefCallback { public: enum Type { HEADERS_AVAILABLE, BYTES_AVAILABLE, }; explicit CefResourceRequestJobCallback(CefResourceRequestJob* job, Type type) : job_(job), type_(type), dest_(NULL), dest_size_(0) {} void Continue() override { // Continue asynchronously. CEF_POST_TASK(CEF_IOT, base::Bind(&CefResourceRequestJobCallback::ContinueOnIOThread, this)); } void Cancel() override { // Cancel asynchronously. CEF_POST_TASK(CEF_IOT, base::Bind(&CefResourceRequestJobCallback::CancelOnIOThread, this)); } void Detach() { CEF_REQUIRE_IOT(); job_ = NULL; } void SetDestination(net::IOBuffer* dest, int dest_size) { CEF_REQUIRE_IOT(); // Should not be called multiple times while IO is pending. DCHECK(!dest_); dest_ = dest; dest_size_ = dest_size; } private: void ContinueOnIOThread() { CEF_REQUIRE_IOT(); // Return early if the callback has already been detached. if (!job_) return; if (type_ == HEADERS_AVAILABLE) { // Callback for headers available. if (!job_->has_response_started()) { // Send header information. job_->SendHeaders(); } // This type of callback only ever needs to be called once. Detach(); } else if (type_ == BYTES_AVAILABLE) { // Callback for bytes available. if (job_->has_response_started() && job_->GetStatus().is_io_pending()) { // Read the bytes. They should be available but, if not, wait again. int bytes_read = job_->ReadRawData(dest_, dest_size_); if (bytes_read == net::ERR_IO_PENDING) { // Still pending, nothing to do... } else if (bytes_read >= 0) { // Must clear the members here because they may be reset as a result // of calling ReadRawDataComplete. dest_size_ = 0; dest_ = NULL; // Notify about the available bytes. If bytes_read > 0 then // ReadRawData may be called from URLRequest::Read. If bytes_read == 0 // then Kill will be called from the URLRequest destructor. job_->ReadRawDataComplete(bytes_read); } else { // Failed due to an error. NOTREACHED() << "ReadRawData returned error " << bytes_read; job_->ReadRawDataComplete(bytes_read); Detach(); } } } } void CancelOnIOThread() { CEF_REQUIRE_IOT(); if (job_) job_->Kill(); } CefResourceRequestJob* job_; Type type_; net::IOBuffer* dest_; int dest_size_; IMPLEMENT_REFCOUNTING(CefResourceRequestJobCallback); }; CefResourceRequestJob::CefResourceRequestJob( net::URLRequest* request, net::NetworkDelegate* network_delegate, CefRefPtr handler) : net::URLRequestJob(request, network_delegate), handler_(handler), done_(false), remaining_bytes_(0), sent_bytes_(0), response_cookies_save_index_(0), weak_factory_(this) { } CefResourceRequestJob::~CefResourceRequestJob() { } void CefResourceRequestJob::Start() { CEF_REQUIRE_IOT(); request_start_time_ = base::Time::Now(); cef_request_ = CefRequest::Create(); // Populate the request data. static_cast(cef_request_.get())->Set(request_); // Add default headers if not already specified. const net::URLRequestContext* context = request_->context(); if (context) { CefRequest::HeaderMap headerMap; cef_request_->GetHeaderMap(headerMap); bool changed = false; const net::HttpUserAgentSettings* ua_settings = context->http_user_agent_settings(); if (ua_settings) { if (SetHeaderIfMissing(headerMap, net::HttpRequestHeaders::kAcceptLanguage, ua_settings->GetAcceptLanguage())) { changed = true; } if (SetHeaderIfMissing(headerMap, net::HttpRequestHeaders::kUserAgent, ua_settings->GetUserAgent())) { changed = true; } } if (changed) cef_request_->SetHeaderMap(headerMap); } AddCookieHeaderAndStart(); } void CefResourceRequestJob::Kill() { CEF_REQUIRE_IOT(); if (!done_) { // Notify the handler that the request has been canceled. handler_->Cancel(); } if (callback_.get()) { callback_->Detach(); callback_ = NULL; } net::URLRequestJob::Kill(); } // This method will be called by URLRequestJob::Read and our callback. // It can indicate the following states: // 1. Return ERR_IO_PENDING, and call ReadRawDataComplete when the read // completes in any way, or // 2. Return a count of bytes read >= 0, indicating synchronous success, or // 3. Return another error code < 0, indicating synchronous failure. int CefResourceRequestJob::ReadRawData(net::IOBuffer* dest, int dest_size) { CEF_REQUIRE_IOT(); DCHECK_NE(dest_size, 0); if (remaining_bytes_ == 0) { // No more data to read. DoneWithRequest(); return 0; } else if (remaining_bytes_ > 0 && remaining_bytes_ < dest_size) { // The handler knows the content size beforehand. dest_size = static_cast(remaining_bytes_); } if (!callback_.get()) { // Create the bytes available callback that will be used until the request // is completed. callback_ = new CefResourceRequestJobCallback(this, CefResourceRequestJobCallback::BYTES_AVAILABLE); } // Read response data from the handler. int bytes_read = 0; bool rv = handler_->ReadResponse(dest->data(), dest_size, bytes_read, callback_.get()); if (!rv) { // The handler has indicated completion of the request. DoneWithRequest(); return 0; } else if (bytes_read == 0) { // Continue reading asynchronously. May happen multiple times in a row so // only set destination members the first time. if (!GetStatus().is_io_pending()) callback_->SetDestination(dest, dest_size); return net::ERR_IO_PENDING; } else if (bytes_read > dest_size) { // Normalize the return value. bytes_read = dest_size; } sent_bytes_ += bytes_read; if (remaining_bytes_ > 0) remaining_bytes_ -= bytes_read; // Continue calling this method. return bytes_read; } void CefResourceRequestJob::GetResponseInfo(net::HttpResponseInfo* info) { CEF_REQUIRE_IOT(); info->headers = GetResponseHeaders(); } void CefResourceRequestJob::GetLoadTimingInfo( net::LoadTimingInfo* load_timing_info) const { // If haven't made it far enough to receive any headers, don't return // anything. This makes for more consistent behavior in the case of errors. if (!response_.get() || receive_headers_end_.is_null()) return; load_timing_info->request_start_time = request_start_time_; load_timing_info->receive_headers_end = receive_headers_end_; } bool CefResourceRequestJob::GetResponseCookies( std::vector* cookies) { CEF_REQUIRE_IOT(); cookies->clear(); FetchResponseCookies(cookies); return true; } bool CefResourceRequestJob::IsRedirectResponse(GURL* location, int* http_status_code) { CEF_REQUIRE_IOT(); if (redirect_url_.is_valid()) { // Redirect to the new URL. *http_status_code = 303; location->Swap(&redirect_url_); return true; } if (response_.get()) { // Check for HTTP 302 or HTTP 303 redirect. int status = response_->GetStatus(); if (status == 302 || status == 303) { CefResponse::HeaderMap headerMap; response_->GetHeaderMap(headerMap); CefRequest::HeaderMap::iterator iter = headerMap.find("Location"); if (iter != headerMap.end()) { GURL new_url = GURL(std::string(iter->second)); *http_status_code = status; location->Swap(&new_url); return true; } } } return false; } bool CefResourceRequestJob::GetMimeType(std::string* mime_type) const { CEF_REQUIRE_IOT(); if (response_.get()) *mime_type = response_->GetMimeType(); return true; } void CefResourceRequestJob::SendHeaders() { CEF_REQUIRE_IOT(); // Clear the headers available callback. callback_ = NULL; // We may have been orphaned... if (!request()) return; response_ = new CefResponseImpl(); remaining_bytes_ = 0; // Set the response mime type if it can be determined from the file extension. if (request_->url().has_path()) { const std::string& path = request_->url().path(); size_t found = path.find_last_of("."); if (found != std::string::npos) { std::string suggest_mime_type; if (net::GetWellKnownMimeTypeFromExtension( base::FilePath::FromUTF8Unsafe(path.substr(found + 1)).value(), &suggest_mime_type)) { response_->SetMimeType(suggest_mime_type); } } } CefString redirectUrl; // Get header information from the handler. handler_->GetResponseHeaders(response_, remaining_bytes_, redirectUrl); receive_headers_end_ = base::TimeTicks::Now(); if (!redirectUrl.empty()) { std::string redirectUrlStr = redirectUrl; redirect_url_ = GURL(redirectUrlStr); } if (remaining_bytes_ > 0) set_expected_content_size(remaining_bytes_); // Continue processing the request. SaveCookiesAndNotifyHeadersComplete(); } void CefResourceRequestJob::AddCookieHeaderAndStart() { // If the request was destroyed, then there is no more work to do. if (!request_) return; net::CookieStore* cookie_store = request_->context()->cookie_store(); if (cookie_store && !(request_->load_flags() & net::LOAD_DO_NOT_SEND_COOKIES)) { net::CookieMonster* cookie_monster = cookie_store->GetCookieMonster(); if (cookie_monster) { cookie_monster->GetAllCookiesForURLAsync( request_->url(), base::Bind(&CefResourceRequestJob::CheckCookiePolicyAndLoad, weak_factory_.GetWeakPtr())); } else { DoLoadCookies(); } } else { DoStartTransaction(); } } void CefResourceRequestJob::DoLoadCookies() { net::CookieOptions options; options.set_include_httponly(); request_->context()->cookie_store()->GetCookiesWithOptionsAsync( request_->url(), options, base::Bind(&CefResourceRequestJob::OnCookiesLoaded, weak_factory_.GetWeakPtr())); } void CefResourceRequestJob::CheckCookiePolicyAndLoad( const net::CookieList& cookie_list) { bool can_get_cookies = CanGetCookies(cookie_list); if (can_get_cookies) { net::CookieList::const_iterator it = cookie_list.begin(); for (; it != cookie_list.end(); ++it) { CefCookie cookie; if (!CefCookieManagerImpl::GetCefCookie(*it, cookie) || !handler_->CanGetCookie(cookie)) { can_get_cookies = false; break; } } } if (can_get_cookies) DoLoadCookies(); else DoStartTransaction(); } void CefResourceRequestJob::OnCookiesLoaded(const std::string& cookie_line) { if (!cookie_line.empty()) { CefRequest::HeaderMap headerMap; cef_request_->GetHeaderMap(headerMap); headerMap.insert( std::make_pair(net::HttpRequestHeaders::kCookie, cookie_line)); cef_request_->SetHeaderMap(headerMap); } DoStartTransaction(); } void CefResourceRequestJob::DoStartTransaction() { // We may have been canceled while retrieving cookies. if (GetStatus().is_success()) { StartTransaction(); } else { NotifyCanceled(); } } void CefResourceRequestJob::StartTransaction() { // Create the callback that will be used to notify when header information is // available. callback_ = new CefResourceRequestJobCallback(this, CefResourceRequestJobCallback::HEADERS_AVAILABLE); // Protect against deletion of this object. base::WeakPtr weak_ptr(weak_factory_.GetWeakPtr()); // Handler can decide whether to process the request. bool rv = handler_->ProcessRequest(cef_request_, callback_.get()); if (weak_ptr.get() && !rv) { // Cancel the request. NotifyCanceled(); } } net::HttpResponseHeaders* CefResourceRequestJob::GetResponseHeaders() { DCHECK(response_.get()); if (!response_headers_.get()) { CefResponseImpl* responseImpl = static_cast(response_.get()); response_headers_ = responseImpl->GetResponseHeaders(); } return response_headers_.get(); } void CefResourceRequestJob::SaveCookiesAndNotifyHeadersComplete() { if (request_->load_flags() & net::LOAD_DO_NOT_SAVE_COOKIES) { NotifyHeadersComplete(); return; } response_cookies_.clear(); response_cookies_save_index_ = 0; FetchResponseCookies(&response_cookies_); // Now, loop over the response cookies, and attempt to persist each. SaveNextCookie(); } void CefResourceRequestJob::SaveNextCookie() { if (response_cookies_save_index_ == response_cookies_.size()) { response_cookies_.clear(); response_cookies_save_index_ = 0; NotifyHeadersComplete(); return; } net::CookieOptions options; options.set_include_httponly(); bool can_set_cookie = CanSetCookie( response_cookies_[response_cookies_save_index_], &options); if (can_set_cookie) { CefCookie cookie; if (CefCookieManagerImpl::GetCefCookie(request_->url(), response_cookies_[response_cookies_save_index_], cookie)) { can_set_cookie = handler_->CanSetCookie(cookie); } else { can_set_cookie = false; } } if (can_set_cookie) { request_->context()->cookie_store()->SetCookieWithOptionsAsync( request_->url(), response_cookies_[response_cookies_save_index_], options, base::Bind(&CefResourceRequestJob::OnCookieSaved, weak_factory_.GetWeakPtr())); return; } CookieHandled(); } void CefResourceRequestJob::OnCookieSaved(bool cookie_status) { CookieHandled(); } void CefResourceRequestJob::CookieHandled() { response_cookies_save_index_++; // We may have been canceled within OnSetCookie. if (GetStatus().is_success()) { SaveNextCookie(); } else { NotifyCanceled(); } } void CefResourceRequestJob::FetchResponseCookies( std::vector* cookies) { const std::string name = "Set-Cookie"; std::string value; size_t iter = 0; net::HttpResponseHeaders* headers = GetResponseHeaders(); while (headers->EnumerateHeader(&iter, name, &value)) { if (!value.empty()) cookies->push_back(value); } } void CefResourceRequestJob::DoneWithRequest() { if (done_) return; done_ = true; if (request_) request_->set_received_response_content_length(sent_bytes_); }