| // Copyright 2015 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "cobalt/loader/net_fetcher.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/trace_event/trace_event.h" |
| #include "cobalt/base/polymorphic_downcast.h" |
| #include "cobalt/loader/cors_preflight.h" |
| #include "cobalt/loader/fetch_interceptor_coordinator.h" |
| #include "cobalt/loader/url_fetcher_string_writer.h" |
| #include "cobalt/network/network_module.h" |
| #include "net/base/mime_util.h" |
| #include "net/url_request/url_fetcher.h" |
| #if defined(STARBOARD) |
| #include "starboard/configuration.h" |
| #if SB_HAS(CORE_DUMP_HANDLER_SUPPORT) |
| #define HANDLE_CORE_DUMP |
| #include "base/lazy_instance.h" |
| #include STARBOARD_CORE_DUMP_HANDLER_INCLUDE |
| #endif // SB_HAS(CORE_DUMP_HANDLER_SUPPORT) |
| #endif // defined(STARBOARD) |
| |
| namespace cobalt { |
| namespace loader { |
| |
| namespace { |
| |
| #if defined(HANDLE_CORE_DUMP) |
| |
| class NetFetcherLog { |
| public: |
| NetFetcherLog() : total_fetched_bytes_(0) { |
| SbCoreDumpRegisterHandler(CoreDumpHandler, this); |
| } |
| ~NetFetcherLog() { SbCoreDumpUnregisterHandler(CoreDumpHandler, this); } |
| |
| static void CoreDumpHandler(void* context) { |
| SbCoreDumpLogInteger( |
| "NetFetcher total fetched bytes", |
| static_cast<NetFetcherLog*>(context)->total_fetched_bytes_); |
| } |
| |
| void IncrementFetchedBytes(int length) { total_fetched_bytes_ += length; } |
| |
| private: |
| int total_fetched_bytes_; |
| DISALLOW_COPY_AND_ASSIGN(NetFetcherLog); |
| }; |
| |
| base::LazyInstance<NetFetcherLog>::DestructorAtExit net_fetcher_log = |
| LAZY_INSTANCE_INITIALIZER; |
| |
| #endif // defined(HANDLE_CORE_DUMP) |
| |
| bool IsResponseCodeSuccess(int response_code) { |
| // NetFetcher only considers success to be if the network request |
| // was successful *and* we get a 2xx response back. |
| // We also accept a response code of -1 for URLs where response code is not |
| // meaningful, like "data:" |
| return response_code == -1 || response_code / 100 == 2; |
| } |
| |
| // Returns true if |mime_type| is allowed for script-like destinations. |
| // See: |
| // https://fetch.spec.whatwg.org/#request-destination-script-like |
| // https://fetch.spec.whatwg.org/#should-response-to-request-be-blocked-due-to-mime-type? |
| bool AllowMimeTypeAsScript(const std::string& mime_type) { |
| if (net::MatchesMimeType("audio/*", mime_type)) return false; |
| if (net::MatchesMimeType("image/*", mime_type)) return false; |
| if (net::MatchesMimeType("video/*", mime_type)) return false; |
| if (net::MatchesMimeType("text/csv", mime_type)) return false; |
| return true; |
| } |
| |
| } // namespace |
| |
| NetFetcher::NetFetcher(const GURL& url, bool main_resource, |
| const csp::SecurityCallback& security_callback, |
| Handler* handler, |
| const network::NetworkModule* network_module, |
| const Options& options, RequestMode request_mode, |
| const Origin& origin) |
| : Fetcher(handler), |
| security_callback_(security_callback), |
| ALLOW_THIS_IN_INITIALIZER_LIST(start_callback_( |
| base::Bind(&NetFetcher::Start, base::Unretained(this)))), |
| cors_policy_(network_module->network_delegate()->cors_policy()), |
| request_cross_origin_(false), |
| origin_(origin), |
| request_script_(options.resource_type == disk_cache::kUncompiledScript), |
| task_runner_(base::ThreadTaskRunnerHandle::Get()), |
| skip_fetch_intercept_(options.skip_fetch_intercept), |
| will_destroy_current_message_loop_(false), |
| main_resource_(main_resource) { |
| url_fetcher_ = net::URLFetcher::Create(url, options.request_method, this); |
| if (!options.headers.IsEmpty()) { |
| url_fetcher_->SetExtraRequestHeaders(options.headers.ToString()); |
| } |
| url_fetcher_->SetRequestContext( |
| network_module->url_request_context_getter().get()); |
| std::unique_ptr<URLFetcherStringWriter> download_data_writer( |
| new URLFetcherStringWriter()); |
| url_fetcher_->SaveResponseWithWriter(std::move(download_data_writer)); |
| if (request_mode != kNoCORSMode && !url.SchemeIs("data") && |
| origin != Origin(url)) { |
| request_cross_origin_ = true; |
| url_fetcher_->AddExtraRequestHeader("Origin:" + origin.SerializedOrigin()); |
| } |
| std::string content_type = |
| std::string(net::HttpRequestHeaders::kResourceType) + ":" + |
| std::to_string(options.resource_type); |
| url_fetcher_->AddExtraRequestHeader(content_type); |
| if ((request_cross_origin_ && |
| (request_mode == kCORSModeSameOriginCredentials)) || |
| request_mode == kCORSModeOmitCredentials) { |
| const uint32 kDisableCookiesLoadFlags = |
| net::LOAD_NORMAL | net::LOAD_DO_NOT_SAVE_COOKIES | |
| net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SEND_AUTH_DATA; |
| url_fetcher_->SetLoadFlags(kDisableCookiesLoadFlags); |
| } |
| network_module->AddClientHintHeaders(*url_fetcher_, network::kCallTypeLoader); |
| |
| // Delay the actual start until this function is complete. Otherwise we might |
| // call handler's callbacks at an unexpected time- e.g. receiving OnError() |
| // while a loader is still being constructed. |
| base::ThreadTaskRunnerHandle::Get()->PostTask(FROM_HERE, |
| start_callback_.callback()); |
| base::MessageLoop::current()->AddDestructionObserver(this); |
| } |
| |
| void NetFetcher::WillDestroyCurrentMessageLoop() { |
| will_destroy_current_message_loop_.store(true); |
| } |
| |
| void NetFetcher::Start() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| const GURL& original_url = url_fetcher_->GetOriginalURL(); |
| TRACE_EVENT1("cobalt::loader", "NetFetcher::Start", "url", |
| original_url.spec()); |
| if (security_callback_.is_null() || |
| security_callback_.Run(original_url, false /* did not redirect */)) { |
| if (skip_fetch_intercept_) { |
| url_fetcher_->Start(); |
| return; |
| } |
| FetchInterceptorCoordinator::GetInstance()->TryIntercept( |
| original_url, main_resource_, url_fetcher_->GetRequestHeaders(), |
| task_runner_, |
| base::BindOnce(&NetFetcher::OnFetchIntercepted, AsWeakPtr()), |
| base::BindOnce(&NetFetcher::ReportLoadTimingInfo, AsWeakPtr()), |
| base::BindOnce(&NetFetcher::InterceptFallback, AsWeakPtr())); |
| |
| } else { |
| std::string msg(base::StringPrintf("URL %s rejected by security policy.", |
| original_url.spec().c_str())); |
| return HandleError(msg).InvalidateThis(); |
| } |
| } |
| |
| void NetFetcher::InterceptFallback() { url_fetcher_->Start(); } |
| |
| void NetFetcher::OnFetchIntercepted(std::unique_ptr<std::string> body) { |
| if (will_destroy_current_message_loop_.load()) { |
| return; |
| } |
| if (task_runner_ != base::ThreadTaskRunnerHandle::Get()) { |
| task_runner_->PostTask( |
| FROM_HERE, base::BindOnce(&NetFetcher::OnFetchIntercepted, |
| base::Unretained(this), std::move(body))); |
| return; |
| } |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| if (!body || body->empty()) { |
| url_fetcher_->Start(); |
| return; |
| } |
| handler()->OnReceivedPassed(this, std::make_unique<std::string>(*body)); |
| handler()->OnDone(this); |
| } |
| |
| void NetFetcher::OnURLFetchResponseStarted(const net::URLFetcher* source) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| TRACE_EVENT1("cobalt::loader", "NetFetcher::OnURLFetchResponseStarted", "url", |
| url_fetcher_->GetOriginalURL().spec()); |
| if (source->GetURL() != source->GetOriginalURL()) { |
| // A redirect occurred. Re-check the security policy. |
| if (!security_callback_.is_null() && |
| !security_callback_.Run(source->GetURL(), true /* did redirect */)) { |
| std::string msg(base::StringPrintf( |
| "URL %s rejected by security policy after a redirect from %s.", |
| source->GetURL().spec().c_str(), |
| source->GetOriginalURL().spec().c_str())); |
| return HandleError(msg).InvalidateThis(); |
| } |
| } |
| |
| if ((handler()->OnResponseStarted(this, source->GetResponseHeaders()) == |
| kLoadResponseAbort) || |
| (!IsResponseCodeSuccess(source->GetResponseCode()))) { |
| std::string msg(base::StringPrintf("URL %s aborted or failed with code %d", |
| source->GetURL().spec().c_str(), |
| source->GetResponseCode())); |
| return HandleError(msg).InvalidateThis(); |
| } |
| |
| // net::URLFetcher can not guarantee GetResponseHeaders() always return |
| // non-null pointer. |
| if (request_cross_origin_ && |
| (!source->GetResponseHeaders() || |
| !CORSPreflight::CORSCheck(*source->GetResponseHeaders(), |
| origin_.SerializedOrigin(), false, |
| cors_policy_))) { |
| std::string msg(base::StringPrintf( |
| "Cross origin request to %s was rejected by Same-Origin-Policy", |
| source->GetURL().spec().c_str())); |
| return HandleError(msg).InvalidateThis(); |
| } |
| |
| if (request_script_ && source->GetResponseHeaders()) { |
| std::string mime_type; |
| if (source->GetResponseHeaders()->GetMimeType(&mime_type) && |
| !AllowMimeTypeAsScript(mime_type)) { |
| std::string msg(base::StringPrintf( |
| "Refused to execute script from '%s' because its MIME type ('%s')" |
| " is not executable.", |
| source->GetURL().spec().c_str(), mime_type.c_str())); |
| return HandleError(msg).InvalidateThis(); |
| } |
| } |
| |
| last_url_origin_ = Origin(source->GetURL()); |
| } |
| |
| void NetFetcher::OnURLFetchComplete(const net::URLFetcher* source) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| TRACE_EVENT1("cobalt::loader", "NetFetcher::OnURLFetchComplete", "url", |
| url_fetcher_->GetOriginalURL().spec()); |
| const net::URLRequestStatus& status = source->GetStatus(); |
| const int response_code = source->GetResponseCode(); |
| if (status.is_success() && IsResponseCodeSuccess(response_code)) { |
| auto* download_data_writer = |
| base::polymorphic_downcast<URLFetcherStringWriter*>( |
| source->GetResponseWriter()); |
| std::string data; |
| download_data_writer->GetAndResetData(&data); |
| if (!data.empty()) { |
| DLOG(INFO) << "in OnURLFetchComplete data still has bytes: " |
| << data.size(); |
| handler()->OnReceivedPassed( |
| this, std::unique_ptr<std::string>(new std::string(std::move(data)))); |
| } |
| handler()->OnDone(this); |
| } else { |
| // Check for response codes and errors that are considered transient. These |
| // are the ones that net::URLFetcherCore is willing to attempt retries on, |
| // along with ERR_NAME_RESOLUTION_FAILED, which indicates a socket error. |
| if (response_code >= 500 || |
| status.error() == net::ERR_TEMPORARILY_THROTTLED || |
| status.error() == net::ERR_NETWORK_CHANGED || |
| status.error() == net::ERR_NAME_RESOLUTION_FAILED || |
| status.error() == net::ERR_CONNECTION_RESET || |
| status.error() == net::ERR_CONNECTION_CLOSED || |
| status.error() == net::ERR_CONNECTION_ABORTED) { |
| did_fail_from_transient_error_ = true; |
| } |
| |
| std::string msg(base::StringPrintf( |
| "NetFetcher error on %s: %s, response code %d", |
| source->GetURL().spec().c_str(), |
| net::ErrorToString(status.error()).c_str(), response_code)); |
| return HandleError(msg).InvalidateThis(); |
| } |
| } |
| |
| void NetFetcher::OnURLFetchDownloadProgress(const net::URLFetcher* source, |
| int64_t current, int64_t total, |
| int64_t current_network_bytes) { |
| if (IsResponseCodeSuccess(source->GetResponseCode())) { |
| auto* download_data_writer = |
| base::polymorphic_downcast<URLFetcherStringWriter*>( |
| source->GetResponseWriter()); |
| std::string data; |
| download_data_writer->GetAndResetData(&data); |
| if (data.empty()) { |
| return; |
| } |
| #if defined(HANDLE_CORE_DUMP) |
| net_fetcher_log.Get().IncrementFetchedBytes( |
| static_cast<int>(data.length())); |
| #endif |
| handler()->OnReceivedPassed( |
| this, std::unique_ptr<std::string>(new std::string(std::move(data)))); |
| } |
| } |
| |
| void NetFetcher::ReportLoadTimingInfo(const net::LoadTimingInfo& timing_info) { |
| handler()->SetLoadTimingInfo(timing_info); |
| } |
| |
| NetFetcher::~NetFetcher() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| start_callback_.Cancel(); |
| if (!will_destroy_current_message_loop_.load()) { |
| base::MessageLoop::current()->RemoveDestructionObserver(this); |
| } |
| } |
| |
| NetFetcher::ReturnWrapper NetFetcher::HandleError(const std::string& message) { |
| url_fetcher_.reset(); |
| handler()->OnError(this, message); |
| return ReturnWrapper(); |
| } |
| |
| } // namespace loader |
| } // namespace cobalt |