// Copyright (c) 2011 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 "net/url_request/url_request_http_job.h" #include "base/base_switches.h" #include "base/command_line.h" #include "base/compiler_specific.h" #include "base/file_util.h" #include "base/file_version_info.h" #include "base/message_loop.h" #include "base/metrics/field_trial.h" #include "base/metrics/histogram.h" #include "base/rand_util.h" #include "base/string_util.h" #include "base/time.h" #include "net/base/cert_status_flags.h" #include "net/base/cookie_policy.h" #include "net/base/cookie_store.h" #include "net/base/filter.h" #include "net/base/host_port_pair.h" #include "net/base/load_flags.h" #include "net/base/mime_util.h" #include "net/base/net_errors.h" #include "net/base/net_util.h" #include "net/base/sdch_manager.h" #include "net/base/ssl_cert_request_info.h" #include "net/base/transport_security_state.h" #include "net/http/http_request_headers.h" #include "net/http/http_response_headers.h" #include "net/http/http_response_info.h" #include "net/http/http_transaction.h" #include "net/http/http_transaction_factory.h" #include "net/http/http_util.h" #include "net/url_request/https_prober.h" #include "net/url_request/url_request.h" #include "net/url_request/url_request_context.h" #include "net/url_request/url_request_error_job.h" #include "net/url_request/url_request_redirect_job.h" #include "net/url_request/url_request_throttler_header_adapter.h" #include "net/url_request/url_request_throttler_manager.h" static const char kAvailDictionaryHeader[] = "Avail-Dictionary"; // When histogramming results related to SDCH and/or an SDCH latency test, the // number of packets for which we need to record arrival times so as to // calculate interpacket latencies. We currently are only looking at the // first few packets, as we're monitoring the impact of the initial TCP // congestion window on stalling of transmissions. static const size_t kSdchPacketHistogramCount = 5; namespace net { namespace { class HTTPSProberDelegateImpl : public HTTPSProberDelegate { public: HTTPSProberDelegateImpl(const std::string& host, int max_age, bool include_subdomains, TransportSecurityState* sts) : host_(host), max_age_(max_age), include_subdomains_(include_subdomains), sts_(sts) { } virtual void ProbeComplete(bool result) { if (result) { base::Time current_time(base::Time::Now()); base::TimeDelta max_age_delta = base::TimeDelta::FromSeconds(max_age_); TransportSecurityState::DomainState domain_state; domain_state.expiry = current_time + max_age_delta; domain_state.mode = TransportSecurityState::DomainState::MODE_OPPORTUNISTIC; domain_state.include_subdomains = include_subdomains_; sts_->EnableHost(host_, domain_state); } delete this; } private: const std::string host_; const int max_age_; const bool include_subdomains_; scoped_refptr<TransportSecurityState> sts_; }; } // namespace class URLRequestHttpJob::HttpFilterContext : public FilterContext { public: explicit HttpFilterContext(URLRequestHttpJob* job); virtual ~HttpFilterContext(); // FilterContext implementation. virtual bool GetMimeType(std::string* mime_type) const; virtual bool GetURL(GURL* gurl) const; virtual base::Time GetRequestTime() const; virtual bool IsCachedContent() const; virtual bool IsDownload() const; virtual bool IsSdchResponse() const; virtual int64 GetByteReadCount() const; virtual int GetResponseCode() const; virtual void RecordPacketStats(StatisticSelector statistic) const; private: URLRequestHttpJob* job_; DISALLOW_COPY_AND_ASSIGN(HttpFilterContext); }; URLRequestHttpJob::HttpFilterContext::HttpFilterContext(URLRequestHttpJob* job) : job_(job) { DCHECK(job_); } URLRequestHttpJob::HttpFilterContext::~HttpFilterContext() { } bool URLRequestHttpJob::HttpFilterContext::GetMimeType( std::string* mime_type) const { return job_->GetMimeType(mime_type); } bool URLRequestHttpJob::HttpFilterContext::GetURL(GURL* gurl) const { if (!job_->request()) return false; *gurl = job_->request()->url(); return true; } base::Time URLRequestHttpJob::HttpFilterContext::GetRequestTime() const { return job_->request() ? job_->request()->request_time() : base::Time(); } bool URLRequestHttpJob::HttpFilterContext::IsCachedContent() const { return job_->is_cached_content_; } bool URLRequestHttpJob::HttpFilterContext::IsDownload() const { return (job_->request_info_.load_flags & LOAD_IS_DOWNLOAD) != 0; } bool URLRequestHttpJob::HttpFilterContext::IsSdchResponse() const { return job_->sdch_dictionary_advertised_; } int64 URLRequestHttpJob::HttpFilterContext::GetByteReadCount() const { return job_->filter_input_byte_count(); } int URLRequestHttpJob::HttpFilterContext::GetResponseCode() const { return job_->GetResponseCode(); } void URLRequestHttpJob::HttpFilterContext::RecordPacketStats( StatisticSelector statistic) const { job_->RecordPacketStats(statistic); } // TODO(darin): make sure the port blocking code is not lost // static URLRequestJob* URLRequestHttpJob::Factory(URLRequest* request, const std::string& scheme) { DCHECK(scheme == "http" || scheme == "https"); int port = request->url().IntPort(); if (!IsPortAllowedByDefault(port) && !IsPortAllowedByOverride(port)) return new URLRequestErrorJob(request, ERR_UNSAFE_PORT); if (!request->context() || !request->context()->http_transaction_factory()) { NOTREACHED() << "requires a valid context"; return new URLRequestErrorJob(request, ERR_INVALID_ARGUMENT); } TransportSecurityState::DomainState domain_state; if (scheme == "http" && request->context()->transport_security_state() && request->context()->transport_security_state()->IsEnabledForHost( &domain_state, request->url().host(), request->context()->IsSNIAvailable())) { if (domain_state.mode == TransportSecurityState::DomainState::MODE_STRICT) { DCHECK_EQ(request->url().scheme(), "http"); url_canon::Replacements<char> replacements; static const char kNewScheme[] = "https"; replacements.SetScheme(kNewScheme, url_parse::Component(0, strlen(kNewScheme))); GURL new_location = request->url().ReplaceComponents(replacements); return new URLRequestRedirectJob(request, new_location); } else { // TODO(agl): implement opportunistic HTTPS upgrade. } } return new URLRequestHttpJob(request); } URLRequestHttpJob::URLRequestHttpJob(URLRequest* request) : URLRequestJob(request), response_info_(NULL), response_cookies_save_index_(0), proxy_auth_state_(AUTH_STATE_DONT_NEED_AUTH), server_auth_state_(AUTH_STATE_DONT_NEED_AUTH), ALLOW_THIS_IN_INITIALIZER_LIST(start_callback_( this, &URLRequestHttpJob::OnStartCompleted)), ALLOW_THIS_IN_INITIALIZER_LIST(read_callback_( this, &URLRequestHttpJob::OnReadCompleted)), read_in_progress_(false), transaction_(NULL), throttling_entry_(URLRequestThrottlerManager::GetInstance()-> RegisterRequestUrl(request->url())), sdch_dictionary_advertised_(false), sdch_test_activated_(false), sdch_test_control_(false), is_cached_content_(false), request_creation_time_(), packet_timing_enabled_(false), bytes_observed_in_packets_(0), packet_times_(), request_time_snapshot_(), final_packet_time_(), observed_packet_count_(0), ALLOW_THIS_IN_INITIALIZER_LIST( filter_context_(new HttpFilterContext(this))), ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) { ResetTimer(); } void URLRequestHttpJob::NotifyHeadersComplete() { DCHECK(!response_info_); response_info_ = transaction_->GetResponseInfo(); // Save boolean, as we'll need this info at destruction time, and filters may // also need this info. is_cached_content_ = response_info_->was_cached; if (!is_cached_content_) { URLRequestThrottlerHeaderAdapter response_adapter( response_info_->headers); throttling_entry_->UpdateWithResponse(request_info_.url.host(), &response_adapter); } ProcessStrictTransportSecurityHeader(); if (SdchManager::Global() && SdchManager::Global()->IsInSupportedDomain(request_->url())) { static const std::string name = "Get-Dictionary"; std::string url_text; void* iter = NULL; // TODO(jar): We need to not fetch dictionaries the first time they are // seen, but rather wait until we can justify their usefulness. // For now, we will only fetch the first dictionary, which will at least // require multiple suggestions before we get additional ones for this site. // Eventually we should wait until a dictionary is requested several times // before we even download it (so that we don't waste memory or bandwidth). if (response_info_->headers->EnumerateHeader(&iter, name, &url_text)) { // request_->url() won't be valid in the destructor, so we use an // alternate copy. DCHECK_EQ(request_->url(), request_info_.url); // Resolve suggested URL relative to request url. sdch_dictionary_url_ = request_info_.url.Resolve(url_text); } } // The HTTP transaction may be restarted several times for the purposes // of sending authorization information. Each time it restarts, we get // notified of the headers completion so that we can update the cookie store. if (transaction_->IsReadyToRestartForAuth()) { DCHECK(!response_info_->auth_challenge.get()); RestartTransactionWithAuth(string16(), string16()); return; } URLRequestJob::NotifyHeadersComplete(); } void URLRequestHttpJob::NotifyDone(const URLRequestStatus& status) { RecordCompressionHistograms(); URLRequestJob::NotifyDone(status); } void URLRequestHttpJob::DestroyTransaction() { DCHECK(transaction_.get()); transaction_.reset(); response_info_ = NULL; context_ = NULL; } void URLRequestHttpJob::StartTransaction() { // NOTE: This method assumes that request_info_ is already setup properly. // If we already have a transaction, then we should restart the transaction // with auth provided by username_ and password_. int rv; if (transaction_.get()) { rv = transaction_->RestartWithAuth(username_, password_, &start_callback_); username_.clear(); password_.clear(); } else { DCHECK(request_->context()); DCHECK(request_->context()->http_transaction_factory()); rv = request_->context()->http_transaction_factory()->CreateTransaction( &transaction_); if (rv == OK) { if (!URLRequestThrottlerManager::GetInstance()->enforce_throttling() || !throttling_entry_->IsDuringExponentialBackoff()) { rv = transaction_->Start( &request_info_, &start_callback_, request_->net_log()); } else { // Special error code for the exponential back-off module. rv = ERR_TEMPORARILY_THROTTLED; } // Make sure the context is alive for the duration of the // transaction. context_ = request_->context(); } } if (rv == ERR_IO_PENDING) return; // The transaction started synchronously, but we need to notify the // URLRequest delegate via the message loop. MessageLoop::current()->PostTask( FROM_HERE, method_factory_.NewRunnableMethod( &URLRequestHttpJob::OnStartCompleted, rv)); } void URLRequestHttpJob::AddExtraHeaders() { // TODO(jar): Consider optimizing away SDCH advertising bytes when the URL is // probably an img or such (and SDCH encoding is not likely). bool advertise_sdch = SdchManager::Global() && SdchManager::Global()->IsInSupportedDomain(request_->url()); std::string avail_dictionaries; if (advertise_sdch) { SdchManager::Global()->GetAvailDictionaryList(request_->url(), &avail_dictionaries); // The AllowLatencyExperiment() is only true if we've successfully done a // full SDCH compression recently in this browser session for this host. // Note that for this path, there might be no applicable dictionaries, and // hence we can't participate in the experiment. if (!avail_dictionaries.empty() && SdchManager::Global()->AllowLatencyExperiment(request_->url())) { // We are participating in the test (or control), and hence we'll // eventually record statistics via either SDCH_EXPERIMENT_DECODE or // SDCH_EXPERIMENT_HOLDBACK, and we'll need some packet timing data. packet_timing_enabled_ = true; if (base::RandDouble() < .01) { sdch_test_control_ = true; // 1% probability. advertise_sdch = false; } else { sdch_test_activated_ = true; } } } // Supply Accept-Encoding headers first so that it is more likely that they // will be in the first transmitted packet. This can sometimes make it easier // to filter and analyze the streams to assure that a proxy has not damaged // these headers. Some proxies deliberately corrupt Accept-Encoding headers. if (!advertise_sdch) { // Tell the server what compression formats we support (other than SDCH). request_info_.extra_headers.SetHeader( HttpRequestHeaders::kAcceptEncoding, "gzip,deflate"); } else { // Include SDCH in acceptable list. request_info_.extra_headers.SetHeader( HttpRequestHeaders::kAcceptEncoding, "gzip,deflate,sdch"); if (!avail_dictionaries.empty()) { request_info_.extra_headers.SetHeader( kAvailDictionaryHeader, avail_dictionaries); sdch_dictionary_advertised_ = true; // Since we're tagging this transaction as advertising a dictionary, we'll // definately employ an SDCH filter (or tentative sdch filter) when we get // a response. When done, we'll record histograms via SDCH_DECODE or // SDCH_PASSTHROUGH. Hence we need to record packet arrival times. packet_timing_enabled_ = true; } } URLRequestContext* context = request_->context(); if (context) { // Only add default Accept-Language and Accept-Charset if the request // didn't have them specified. if (!context->accept_language().empty()) { request_info_.extra_headers.SetHeaderIfMissing( HttpRequestHeaders::kAcceptLanguage, context->accept_language()); } if (!context->accept_charset().empty()) { request_info_.extra_headers.SetHeaderIfMissing( HttpRequestHeaders::kAcceptCharset, context->accept_charset()); } } } void URLRequestHttpJob::AddCookieHeaderAndStart() { // No matter what, we want to report our status as IO pending since we will // be notifying our consumer asynchronously via OnStartCompleted. SetStatus(URLRequestStatus(URLRequestStatus::IO_PENDING, 0)); int policy = OK; if (request_info_.load_flags & LOAD_DO_NOT_SEND_COOKIES) { policy = ERR_FAILED; } else if (request_->context()->cookie_policy()) { policy = request_->context()->cookie_policy()->CanGetCookies( request_->url(), request_->first_party_for_cookies()); } OnCanGetCookiesCompleted(policy); } void URLRequestHttpJob::SaveCookiesAndNotifyHeadersComplete() { DCHECK(transaction_.get()); const HttpResponseInfo* response_info = transaction_->GetResponseInfo(); DCHECK(response_info); response_cookies_.clear(); response_cookies_save_index_ = 0; FetchResponseCookies(response_info, &response_cookies_); // Now, loop over the response cookies, and attempt to persist each. SaveNextCookie(); } void URLRequestHttpJob::SaveNextCookie() { if (response_cookies_save_index_ == response_cookies_.size()) { response_cookies_.clear(); response_cookies_save_index_ = 0; SetStatus(URLRequestStatus()); // Clear the IO_PENDING status NotifyHeadersComplete(); return; } // No matter what, we want to report our status as IO pending since we will // be notifying our consumer asynchronously via OnStartCompleted. SetStatus(URLRequestStatus(URLRequestStatus::IO_PENDING, 0)); int policy = OK; if (request_info_.load_flags & LOAD_DO_NOT_SAVE_COOKIES) { policy = ERR_FAILED; } else if (request_->context()->cookie_policy()) { policy = request_->context()->cookie_policy()->CanSetCookie( request_->url(), request_->first_party_for_cookies(), response_cookies_[response_cookies_save_index_]); } OnCanSetCookieCompleted(policy); } void URLRequestHttpJob::FetchResponseCookies( const HttpResponseInfo* response_info, std::vector<std::string>* cookies) { std::string name = "Set-Cookie"; std::string value; void* iter = NULL; while (response_info->headers->EnumerateHeader(&iter, name, &value)) { if (!value.empty()) cookies->push_back(value); } } void URLRequestHttpJob::ProcessStrictTransportSecurityHeader() { DCHECK(response_info_); URLRequestContext* ctx = request_->context(); if (!ctx || !ctx->transport_security_state()) return; const bool https = response_info_->ssl_info.is_valid(); const bool valid_https = https && !IsCertStatusError(response_info_->ssl_info.cert_status); std::string name = "Strict-Transport-Security"; std::string value; int max_age; bool include_subdomains; void* iter = NULL; while (response_info_->headers->EnumerateHeader(&iter, name, &value)) { const bool ok = TransportSecurityState::ParseHeader( value, &max_age, &include_subdomains); if (!ok) continue; // We will only accept strict mode if we saw the header from an HTTPS // connection with no certificate problems. if (!valid_https) continue; base::Time current_time(base::Time::Now()); base::TimeDelta max_age_delta = base::TimeDelta::FromSeconds(max_age); TransportSecurityState::DomainState domain_state; domain_state.expiry = current_time + max_age_delta; domain_state.mode = TransportSecurityState::DomainState::MODE_STRICT; domain_state.include_subdomains = include_subdomains; ctx->transport_security_state()->EnableHost(request_info_.url.host(), domain_state); } // TODO(agl): change this over when we have fixed things at the server end. // The string should be "Opportunistic-Transport-Security"; name = "X-Bodge-Transport-Security"; while (response_info_->headers->EnumerateHeader(&iter, name, &value)) { const bool ok = TransportSecurityState::ParseHeader( value, &max_age, &include_subdomains); if (!ok) continue; // If we saw an opportunistic request over HTTPS, then clearly we can make // HTTPS connections to the host so we should remember this. if (https) { base::Time current_time(base::Time::Now()); base::TimeDelta max_age_delta = base::TimeDelta::FromSeconds(max_age); TransportSecurityState::DomainState domain_state; domain_state.expiry = current_time + max_age_delta; domain_state.mode = TransportSecurityState::DomainState::MODE_SPDY_ONLY; domain_state.include_subdomains = include_subdomains; ctx->transport_security_state()->EnableHost(request_info_.url.host(), domain_state); continue; } if (!request()) break; // At this point, we have a request for opportunistic encryption over HTTP. // In this case we need to probe to check that we can make HTTPS // connections to that host. HTTPSProber* const prober = HTTPSProber::GetInstance(); if (prober->HaveProbed(request_info_.url.host()) || prober->InFlight(request_info_.url.host())) { continue; } HTTPSProberDelegateImpl* delegate = new HTTPSProberDelegateImpl(request_info_.url.host(), max_age, include_subdomains, ctx->transport_security_state()); if (!prober->ProbeHost(request_info_.url.host(), request()->context(), delegate)) { delete delegate; } } } void URLRequestHttpJob::OnCanGetCookiesCompleted(int policy) { // If the request was destroyed, then there is no more work to do. if (request_ && request_->delegate()) { if (request_->context()->cookie_store()) { if (policy == ERR_ACCESS_DENIED) { request_->delegate()->OnGetCookies(request_, true); } else if (policy == OK) { request_->delegate()->OnGetCookies(request_, false); CookieOptions options; options.set_include_httponly(); std::string cookies = request_->context()->cookie_store()->GetCookiesWithOptions( request_->url(), options); if (!cookies.empty()) { request_info_.extra_headers.SetHeader( HttpRequestHeaders::kCookie, cookies); } } } // We may have been canceled within OnGetCookies. if (GetStatus().is_success()) { StartTransaction(); } else { NotifyCanceled(); } } } void URLRequestHttpJob::OnCanSetCookieCompleted(int policy) { // If the request was destroyed, then there is no more work to do. if (request_ && request_->delegate()) { if (request_->context()->cookie_store()) { if (policy == ERR_ACCESS_DENIED) { CookieOptions options; options.set_include_httponly(); request_->delegate()->OnSetCookie( request_, response_cookies_[response_cookies_save_index_], options, true); } else if (policy == OK || policy == OK_FOR_SESSION_ONLY) { // OK to save the current response cookie now. CookieOptions options; options.set_include_httponly(); if (policy == OK_FOR_SESSION_ONLY) options.set_force_session(); request_->context()->cookie_store()->SetCookieWithOptions( request_->url(), response_cookies_[response_cookies_save_index_], options); request_->delegate()->OnSetCookie( request_, response_cookies_[response_cookies_save_index_], options, false); } } response_cookies_save_index_++; // We may have been canceled within OnSetCookie. if (GetStatus().is_success()) { SaveNextCookie(); } else { NotifyCanceled(); } } } void URLRequestHttpJob::OnStartCompleted(int result) { RecordTimer(); // If the request was destroyed, then there is no more work to do. if (!request_ || !request_->delegate()) return; // If the transaction was destroyed, then the job was cancelled, and // we can just ignore this notification. if (!transaction_.get()) return; // Clear the IO_PENDING status SetStatus(URLRequestStatus()); // Take care of any mandates for public key pinning. // TODO(agl): we might have an issue here where a request for foo.example.com // merges into a SPDY connection to www.example.com, and gets a different // certificate. const SSLInfo& ssl_info = transaction_->GetResponseInfo()->ssl_info; if (result == OK && ssl_info.is_valid() && context_->transport_security_state()) { TransportSecurityState::DomainState domain_state; if (context_->transport_security_state()->IsEnabledForHost( &domain_state, request_->url().host(), context_->IsSNIAvailable()) && ssl_info.is_issued_by_known_root && !domain_state.IsChainOfPublicKeysPermitted(ssl_info.public_key_hashes)){ result = ERR_CERT_INVALID; } } if (result == OK) { SaveCookiesAndNotifyHeadersComplete(); } else if (ShouldTreatAsCertificateError(result)) { // We encountered an SSL certificate error. Ask our delegate to decide // what we should do. // TODO(wtc): also pass ssl_info.cert_status, or just pass the whole // ssl_info. request_->delegate()->OnSSLCertificateError( request_, result, transaction_->GetResponseInfo()->ssl_info.cert); } else if (result == ERR_SSL_CLIENT_AUTH_CERT_NEEDED) { request_->delegate()->OnCertificateRequested( request_, transaction_->GetResponseInfo()->cert_request_info); } else { NotifyStartError(URLRequestStatus(URLRequestStatus::FAILED, result)); } } void URLRequestHttpJob::OnReadCompleted(int result) { read_in_progress_ = false; if (result == 0) { NotifyDone(URLRequestStatus()); } else if (result < 0) { NotifyDone(URLRequestStatus(URLRequestStatus::FAILED, result)); } else { // Clear the IO_PENDING status SetStatus(URLRequestStatus()); } NotifyReadComplete(result); } bool URLRequestHttpJob::ShouldTreatAsCertificateError(int result) { if (!IsCertificateError(result)) return false; // Revocation check failures are always certificate errors, even if the host // is using Strict-Transport-Security. if (result == ERR_CERT_UNABLE_TO_CHECK_REVOCATION) return true; // Check whether our context is using Strict-Transport-Security. if (!context_->transport_security_state()) return true; TransportSecurityState::DomainState domain_state; // TODO(agl): don't ignore opportunistic mode. const bool r = context_->transport_security_state()->IsEnabledForHost( &domain_state, request_info_.url.host(), context_->IsSNIAvailable()); return !r || domain_state.mode == TransportSecurityState::DomainState::MODE_OPPORTUNISTIC; } void URLRequestHttpJob::RestartTransactionWithAuth( const string16& username, const string16& password) { username_ = username; password_ = password; // These will be reset in OnStartCompleted. response_info_ = NULL; response_cookies_.clear(); ResetTimer(); // Update the cookies, since the cookie store may have been updated from the // headers in the 401/407. Since cookies were already appended to // extra_headers, we need to strip them out before adding them again. request_info_.extra_headers.RemoveHeader( HttpRequestHeaders::kCookie); AddCookieHeaderAndStart(); } void URLRequestHttpJob::SetUpload(UploadData* upload) { DCHECK(!transaction_.get()) << "cannot change once started"; request_info_.upload_data = upload; } void URLRequestHttpJob::SetExtraRequestHeaders( const HttpRequestHeaders& headers) { DCHECK(!transaction_.get()) << "cannot change once started"; request_info_.extra_headers.CopyFrom(headers); } void URLRequestHttpJob::Start() { DCHECK(!transaction_.get()); // Ensure that we do not send username and password fields in the referrer. GURL referrer(request_->GetSanitizedReferrer()); request_info_.url = request_->url(); request_info_.referrer = referrer; request_info_.method = request_->method(); request_info_.load_flags = request_->load_flags(); request_info_.priority = request_->priority(); request_info_.request_id = request_->identifier(); if (request_->context()) { request_info_.extra_headers.SetHeaderIfMissing( HttpRequestHeaders::kUserAgent, request_->context()->GetUserAgent(request_->url())); } AddExtraHeaders(); #ifdef ANDROID // Attribute network traffic to the UID of the caller request_info_.valid_uid = false; request_info_.calling_uid = 0; if (request_->context()) { uid_t uid; if(request_->context()->getUID(&uid)) { request_info_.valid_uid = true; request_info_.calling_uid = uid; } } #endif AddCookieHeaderAndStart(); } void URLRequestHttpJob::Kill() { if (!transaction_.get()) return; DestroyTransaction(); URLRequestJob::Kill(); } LoadState URLRequestHttpJob::GetLoadState() const { return transaction_.get() ? transaction_->GetLoadState() : LOAD_STATE_IDLE; } uint64 URLRequestHttpJob::GetUploadProgress() const { return transaction_.get() ? transaction_->GetUploadProgress() : 0; } bool URLRequestHttpJob::GetMimeType(std::string* mime_type) const { DCHECK(transaction_.get()); if (!response_info_) return false; return response_info_->headers->GetMimeType(mime_type); } bool URLRequestHttpJob::GetCharset(std::string* charset) { DCHECK(transaction_.get()); if (!response_info_) return false; return response_info_->headers->GetCharset(charset); } void URLRequestHttpJob::GetResponseInfo(HttpResponseInfo* info) { DCHECK(request_); DCHECK(transaction_.get()); if (response_info_) *info = *response_info_; } bool URLRequestHttpJob::GetResponseCookies( std::vector<std::string>* cookies) { DCHECK(transaction_.get()); if (!response_info_) return false; // TODO(darin): Why are we extracting response cookies again? Perhaps we // should just leverage response_cookies_. cookies->clear(); FetchResponseCookies(response_info_, cookies); return true; } int URLRequestHttpJob::GetResponseCode() const { DCHECK(transaction_.get()); if (!response_info_) return -1; return response_info_->headers->response_code(); } Filter* URLRequestHttpJob::SetupFilter() const { DCHECK(transaction_.get()); if (!response_info_) return NULL; std::vector<Filter::FilterType> encoding_types; std::string encoding_type; void* iter = NULL; while (response_info_->headers->EnumerateHeader(&iter, "Content-Encoding", &encoding_type)) { encoding_types.push_back(Filter::ConvertEncodingToType(encoding_type)); } // Even if encoding types are empty, there is a chance that we need to add // some decoding, as some proxies strip encoding completely. In such cases, // we may need to add (for example) SDCH filtering (when the context suggests // it is appropriate). Filter::FixupEncodingTypes(*filter_context_, &encoding_types); return !encoding_types.empty() ? Filter::Factory(encoding_types, *filter_context_) : NULL; } bool URLRequestHttpJob::IsSafeRedirect(const GURL& location) { // We only allow redirects to certain "safe" protocols. This does not // restrict redirects to externally handled protocols. Our consumer would // need to take care of those. if (!URLRequest::IsHandledURL(location)) return true; static const char* kSafeSchemes[] = { "http", "https", "ftp" }; for (size_t i = 0; i < arraysize(kSafeSchemes); ++i) { if (location.SchemeIs(kSafeSchemes[i])) return true; } return false; } bool URLRequestHttpJob::NeedsAuth() { int code = GetResponseCode(); if (code == -1) return false; // Check if we need either Proxy or WWW Authentication. This could happen // because we either provided no auth info, or provided incorrect info. switch (code) { case 407: if (proxy_auth_state_ == AUTH_STATE_CANCELED) return false; proxy_auth_state_ = AUTH_STATE_NEED_AUTH; return true; case 401: if (server_auth_state_ == AUTH_STATE_CANCELED) return false; server_auth_state_ = AUTH_STATE_NEED_AUTH; return true; } return false; } void URLRequestHttpJob::GetAuthChallengeInfo( scoped_refptr<AuthChallengeInfo>* result) { DCHECK(transaction_.get()); DCHECK(response_info_); // sanity checks: DCHECK(proxy_auth_state_ == AUTH_STATE_NEED_AUTH || server_auth_state_ == AUTH_STATE_NEED_AUTH); DCHECK(response_info_->headers->response_code() == 401 || response_info_->headers->response_code() == 407); *result = response_info_->auth_challenge; } void URLRequestHttpJob::SetAuth(const string16& username, const string16& password) { DCHECK(transaction_.get()); // Proxy gets set first, then WWW. if (proxy_auth_state_ == AUTH_STATE_NEED_AUTH) { proxy_auth_state_ = AUTH_STATE_HAVE_AUTH; } else { DCHECK_EQ(server_auth_state_, AUTH_STATE_NEED_AUTH); server_auth_state_ = AUTH_STATE_HAVE_AUTH; } RestartTransactionWithAuth(username, password); } void URLRequestHttpJob::CancelAuth() { // Proxy gets set first, then WWW. if (proxy_auth_state_ == AUTH_STATE_NEED_AUTH) { proxy_auth_state_ = AUTH_STATE_CANCELED; } else { DCHECK_EQ(server_auth_state_, AUTH_STATE_NEED_AUTH); server_auth_state_ = AUTH_STATE_CANCELED; } // These will be reset in OnStartCompleted. response_info_ = NULL; response_cookies_.clear(); ResetTimer(); // OK, let the consumer read the error page... // // Because we set the AUTH_STATE_CANCELED flag, NeedsAuth will return false, // which will cause the consumer to receive OnResponseStarted instead of // OnAuthRequired. // // We have to do this via InvokeLater to avoid "recursing" the consumer. // MessageLoop::current()->PostTask( FROM_HERE, method_factory_.NewRunnableMethod( &URLRequestHttpJob::OnStartCompleted, OK)); } void URLRequestHttpJob::ContinueWithCertificate( X509Certificate* client_cert) { DCHECK(transaction_.get()); DCHECK(!response_info_) << "should not have a response yet"; ResetTimer(); // No matter what, we want to report our status as IO pending since we will // be notifying our consumer asynchronously via OnStartCompleted. SetStatus(URLRequestStatus(URLRequestStatus::IO_PENDING, 0)); int rv = transaction_->RestartWithCertificate(client_cert, &start_callback_); if (rv == ERR_IO_PENDING) return; // The transaction started synchronously, but we need to notify the // URLRequest delegate via the message loop. MessageLoop::current()->PostTask( FROM_HERE, method_factory_.NewRunnableMethod( &URLRequestHttpJob::OnStartCompleted, rv)); } void URLRequestHttpJob::ContinueDespiteLastError() { // If the transaction was destroyed, then the job was cancelled. if (!transaction_.get()) return; DCHECK(!response_info_) << "should not have a response yet"; ResetTimer(); // No matter what, we want to report our status as IO pending since we will // be notifying our consumer asynchronously via OnStartCompleted. SetStatus(URLRequestStatus(URLRequestStatus::IO_PENDING, 0)); int rv = transaction_->RestartIgnoringLastError(&start_callback_); if (rv == ERR_IO_PENDING) return; // The transaction started synchronously, but we need to notify the // URLRequest delegate via the message loop. MessageLoop::current()->PostTask( FROM_HERE, method_factory_.NewRunnableMethod( &URLRequestHttpJob::OnStartCompleted, rv)); } bool URLRequestHttpJob::ReadRawData(IOBuffer* buf, int buf_size, int *bytes_read) { DCHECK_NE(buf_size, 0); DCHECK(bytes_read); DCHECK(!read_in_progress_); int rv = transaction_->Read(buf, buf_size, &read_callback_); if (rv >= 0) { *bytes_read = rv; return true; } if (rv == ERR_IO_PENDING) { read_in_progress_ = true; SetStatus(URLRequestStatus(URLRequestStatus::IO_PENDING, 0)); } else { NotifyDone(URLRequestStatus(URLRequestStatus::FAILED, rv)); } return false; } void URLRequestHttpJob::StopCaching() { if (transaction_.get()) transaction_->StopCaching(); } HostPortPair URLRequestHttpJob::GetSocketAddress() const { return response_info_ ? response_info_->socket_address : HostPortPair(); } URLRequestHttpJob::~URLRequestHttpJob() { DCHECK(!sdch_test_control_ || !sdch_test_activated_); if (!is_cached_content_) { if (sdch_test_control_) RecordPacketStats(FilterContext::SDCH_EXPERIMENT_HOLDBACK); if (sdch_test_activated_) RecordPacketStats(FilterContext::SDCH_EXPERIMENT_DECODE); } // Make sure SDCH filters are told to emit histogram data while // filter_context_ is still alive. DestroyFilters(); if (sdch_dictionary_url_.is_valid()) { // Prior to reaching the destructor, request_ has been set to a NULL // pointer, so request_->url() is no longer valid in the destructor, and we // use an alternate copy |request_info_.url|. SdchManager* manager = SdchManager::Global(); // To be extra safe, since this is a "different time" from when we decided // to get the dictionary, we'll validate that an SdchManager is available. // At shutdown time, care is taken to be sure that we don't delete this // globally useful instance "too soon," so this check is just defensive // coding to assure that IF the system is shutting down, we don't have any // problem if the manager was deleted ahead of time. if (manager) // Defensive programming. manager->FetchDictionary(request_info_.url, sdch_dictionary_url_); } } void URLRequestHttpJob::RecordTimer() { if (request_creation_time_.is_null()) { NOTREACHED() << "The same transaction shouldn't start twice without new timing."; return; } base::TimeDelta to_start = base::Time::Now() - request_creation_time_; request_creation_time_ = base::Time(); static const bool use_prefetch_histogram = base::FieldTrialList::Find("Prefetch") && !base::FieldTrialList::Find("Prefetch")->group_name().empty(); UMA_HISTOGRAM_MEDIUM_TIMES("Net.HttpTimeToFirstByte", to_start); if (use_prefetch_histogram) { UMA_HISTOGRAM_MEDIUM_TIMES( base::FieldTrial::MakeName("Net.HttpTimeToFirstByte", "Prefetch"), to_start); } const bool is_prerender = !!(request_info_.load_flags & LOAD_PRERENDER); if (is_prerender) { UMA_HISTOGRAM_MEDIUM_TIMES("Net.HttpTimeToFirstByte_Prerender", to_start); if (use_prefetch_histogram) { UMA_HISTOGRAM_MEDIUM_TIMES( base::FieldTrial::MakeName("Net.HttpTimeToFirstByte_Prerender", "Prefetch"), to_start); } } else { UMA_HISTOGRAM_MEDIUM_TIMES("Net.HttpTimeToFirstByte_NonPrerender", to_start); if (use_prefetch_histogram) { UMA_HISTOGRAM_MEDIUM_TIMES( base::FieldTrial::MakeName("Net.HttpTimeToFirstByte_NonPrerender", "Prefetch"), to_start); } } } void URLRequestHttpJob::ResetTimer() { if (!request_creation_time_.is_null()) { NOTREACHED() << "The timer was reset before it was recorded."; return; } request_creation_time_ = base::Time::Now(); } void URLRequestHttpJob::UpdatePacketReadTimes() { if (!packet_timing_enabled_) return; if (filter_input_byte_count() <= bytes_observed_in_packets_) { DCHECK_EQ(filter_input_byte_count(), bytes_observed_in_packets_); return; // No new bytes have arrived. } if (!bytes_observed_in_packets_) request_time_snapshot_ = request_ ? request_->request_time() : base::Time(); final_packet_time_ = base::Time::Now(); const size_t kTypicalPacketSize = 1430; while (filter_input_byte_count() > bytes_observed_in_packets_) { ++observed_packet_count_; if (packet_times_.size() < kSdchPacketHistogramCount) { packet_times_.push_back(final_packet_time_); DCHECK_EQ(static_cast<size_t>(observed_packet_count_), packet_times_.size()); } bytes_observed_in_packets_ += kTypicalPacketSize; } // Since packets may not be full, we'll remember the number of bytes we've // accounted for in packets thus far. bytes_observed_in_packets_ = filter_input_byte_count(); } void URLRequestHttpJob::RecordPacketStats( FilterContext::StatisticSelector statistic) const { if (!packet_timing_enabled_ || (final_packet_time_ == base::Time())) return; base::TimeDelta duration = final_packet_time_ - request_time_snapshot_; switch (statistic) { case FilterContext::SDCH_DECODE: { UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Decode_Latency_F_a", duration, base::TimeDelta::FromMilliseconds(20), base::TimeDelta::FromMinutes(10), 100); UMA_HISTOGRAM_COUNTS_100("Sdch3.Network_Decode_Packets_b", static_cast<int>(observed_packet_count_)); UMA_HISTOGRAM_CUSTOM_COUNTS("Sdch3.Network_Decode_Bytes_Processed_b", static_cast<int>(bytes_observed_in_packets_), 500, 100000, 100); if (packet_times_.empty()) return; UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Decode_1st_To_Last_a", final_packet_time_ - packet_times_[0], base::TimeDelta::FromMilliseconds(20), base::TimeDelta::FromMinutes(10), 100); DCHECK_GT(kSdchPacketHistogramCount, 4u); if (packet_times_.size() <= 4) return; UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Decode_1st_To_2nd_c", packet_times_[1] - packet_times_[0], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Decode_2nd_To_3rd_c", packet_times_[2] - packet_times_[1], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Decode_3rd_To_4th_c", packet_times_[3] - packet_times_[2], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Decode_4th_To_5th_c", packet_times_[4] - packet_times_[3], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); return; } case FilterContext::SDCH_PASSTHROUGH: { // Despite advertising a dictionary, we handled non-sdch compressed // content. UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Pass-through_Latency_F_a", duration, base::TimeDelta::FromMilliseconds(20), base::TimeDelta::FromMinutes(10), 100); UMA_HISTOGRAM_COUNTS_100("Sdch3.Network_Pass-through_Packets_b", observed_packet_count_); if (packet_times_.empty()) return; UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Pass-through_1st_To_Last_a", final_packet_time_ - packet_times_[0], base::TimeDelta::FromMilliseconds(20), base::TimeDelta::FromMinutes(10), 100); DCHECK_GT(kSdchPacketHistogramCount, 4u); if (packet_times_.size() <= 4) return; UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Pass-through_1st_To_2nd_c", packet_times_[1] - packet_times_[0], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Pass-through_2nd_To_3rd_c", packet_times_[2] - packet_times_[1], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Pass-through_3rd_To_4th_c", packet_times_[3] - packet_times_[2], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Network_Pass-through_4th_To_5th_c", packet_times_[4] - packet_times_[3], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); return; } case FilterContext::SDCH_EXPERIMENT_DECODE: { UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Experiment_Decode", duration, base::TimeDelta::FromMilliseconds(20), base::TimeDelta::FromMinutes(10), 100); // We already provided interpacket histograms above in the SDCH_DECODE // case, so we don't need them here. return; } case FilterContext::SDCH_EXPERIMENT_HOLDBACK: { UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Experiment_Holdback", duration, base::TimeDelta::FromMilliseconds(20), base::TimeDelta::FromMinutes(10), 100); UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Experiment_Holdback_1st_To_Last_a", final_packet_time_ - packet_times_[0], base::TimeDelta::FromMilliseconds(20), base::TimeDelta::FromMinutes(10), 100); DCHECK_GT(kSdchPacketHistogramCount, 4u); if (packet_times_.size() <= 4) return; UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Experiment_Holdback_1st_To_2nd_c", packet_times_[1] - packet_times_[0], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Experiment_Holdback_2nd_To_3rd_c", packet_times_[2] - packet_times_[1], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Experiment_Holdback_3rd_To_4th_c", packet_times_[3] - packet_times_[2], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); UMA_HISTOGRAM_CLIPPED_TIMES("Sdch3.Experiment_Holdback_4th_To_5th_c", packet_times_[4] - packet_times_[3], base::TimeDelta::FromMilliseconds(1), base::TimeDelta::FromSeconds(10), 100); return; } default: NOTREACHED(); return; } } // The common type of histogram we use for all compression-tracking histograms. #define COMPRESSION_HISTOGRAM(name, sample) \ do { \ UMA_HISTOGRAM_CUSTOM_COUNTS("Net.Compress." name, sample, \ 500, 1000000, 100); \ } while(0) void URLRequestHttpJob::RecordCompressionHistograms() { DCHECK(request_); if (!request_) return; if (is_cached_content_ || // Don't record cached content !GetStatus().is_success() || // Don't record failed content !IsCompressibleContent() || // Only record compressible content !prefilter_bytes_read()) // Zero-byte responses aren't useful. return; // Miniature requests aren't really compressible. Don't count them. const int kMinSize = 16; if (prefilter_bytes_read() < kMinSize) return; // Only record for http or https urls. bool is_http = request_->url().SchemeIs("http"); bool is_https = request_->url().SchemeIs("https"); if (!is_http && !is_https) return; int compressed_B = prefilter_bytes_read(); int decompressed_B = postfilter_bytes_read(); bool was_filtered = HasFilter(); // We want to record how often downloaded resources are compressed. // But, we recognize that different protocols may have different // properties. So, for each request, we'll put it into one of 3 // groups: // a) SSL resources // Proxies cannot tamper with compression headers with SSL. // b) Non-SSL, loaded-via-proxy resources // In this case, we know a proxy might have interfered. // c) Non-SSL, loaded-without-proxy resources // In this case, we know there was no explicit proxy. However, // it is possible that a transparent proxy was still interfering. // // For each group, we record the same 3 histograms. if (is_https) { if (was_filtered) { COMPRESSION_HISTOGRAM("SSL.BytesBeforeCompression", compressed_B); COMPRESSION_HISTOGRAM("SSL.BytesAfterCompression", decompressed_B); } else { COMPRESSION_HISTOGRAM("SSL.ShouldHaveBeenCompressed", decompressed_B); } return; } if (request_->was_fetched_via_proxy()) { if (was_filtered) { COMPRESSION_HISTOGRAM("Proxy.BytesBeforeCompression", compressed_B); COMPRESSION_HISTOGRAM("Proxy.BytesAfterCompression", decompressed_B); } else { COMPRESSION_HISTOGRAM("Proxy.ShouldHaveBeenCompressed", decompressed_B); } return; } if (was_filtered) { COMPRESSION_HISTOGRAM("NoProxy.BytesBeforeCompression", compressed_B); COMPRESSION_HISTOGRAM("NoProxy.BytesAfterCompression", decompressed_B); } else { COMPRESSION_HISTOGRAM("NoProxy.ShouldHaveBeenCompressed", decompressed_B); } } bool URLRequestHttpJob::IsCompressibleContent() const { std::string mime_type; return GetMimeType(&mime_type) && (IsSupportedJavascriptMimeType(mime_type.c_str()) || IsSupportedNonImageMimeType(mime_type.c_str())); } } // namespace net