| // Copyright 2018 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "shill/http_request.h" |
| |
| #include <curl/curl.h> |
| |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| #include <base/check.h> |
| #include <base/functional/bind.h> |
| #include <base/logging.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_util.h> |
| #include <base/time/time.h> |
| #include <base/types/expected.h> |
| #include <brillo/http/http_utils.h> |
| #include <net-base/dns_client.h> |
| #include <net-base/http_url.h> |
| |
| #include "shill/event_dispatcher.h" |
| #include "shill/logging.h" |
| |
| namespace { |
| |
| // The curl error domain for http requests |
| const char kCurlEasyError[] = "curl_easy_error"; |
| |
| // Maximum number of name servers queried in parallel. |
| constexpr int kDNSMaxParallelQueries = 4; |
| // Maximum number of query tries per name server. |
| constexpr int kDNSNumberOfQueries = 3; |
| // Timeout of a single query to a single name server. |
| constexpr base::TimeDelta kDNSTimeoutOfQueries = base::Seconds(2); |
| |
| } // namespace |
| |
| namespace shill { |
| |
| namespace Logging { |
| static auto kModuleLogScope = ScopeLogger::kHTTP; |
| static std::string ObjectID(const HttpRequest* r) { |
| return r->logging_tag(); |
| } |
| } // namespace Logging |
| |
| HttpRequest::HttpRequest( |
| EventDispatcher* dispatcher, |
| std::string_view interface_name, |
| net_base::IPFamily ip_family, |
| const std::vector<net_base::IPAddress>& dns_list, |
| bool allow_non_google_https, |
| std::shared_ptr<brillo::http::Transport> transport, |
| std::unique_ptr<net_base::DNSClientFactory> dns_client_factory) |
| : dispatcher_(dispatcher), |
| ip_family_(ip_family), |
| dns_list_(dns_list), |
| transport_(transport), |
| dns_client_factory_(std::move(dns_client_factory)), |
| request_id_(-1), |
| is_running_(false) { |
| dns_options_.interface = interface_name; |
| // TODO(b/307880493): Tune these parameters based on the technology once |
| // metrics are available. |
| dns_options_.number_of_tries = kDNSNumberOfQueries; |
| dns_options_.per_query_initial_timeout = kDNSTimeoutOfQueries; |
| // b/180521518, Force the transport to bind to |interface_name|. Otherwise, |
| // the request would be routed by default through the current physical default |
| // network. b/288351302: binding to an IP address of the interface name is not |
| // enough to disambiguate all IPv4 multi-network scenarios. |
| transport_->SetInterface(dns_options_.interface); |
| if (allow_non_google_https) { |
| transport_->UseCustomCertificate( |
| brillo::http::Transport::Certificate::kNss); |
| } |
| } |
| |
| HttpRequest::~HttpRequest() { |
| Stop(); |
| } |
| |
| void HttpRequest::Start(std::string_view logging_tag, |
| const net_base::HttpUrl& url, |
| const brillo::http::HeaderList& headers, |
| base::OnceCallback<void(Result result)> callback) { |
| DCHECK(!is_running_); |
| logging_tag_ = logging_tag; |
| url_ = url; |
| headers_ = headers; |
| is_running_ = true; |
| transport_->SetDefaultTimeout(kRequestTimeout); |
| callback_ = std::move(callback); |
| |
| // Name resolution is not needed if the hostname is an IP address literal. |
| if (const auto server_addr = |
| net_base::IPAddress::CreateFromString(url_.host())) { |
| if (server_addr->GetFamily() == ip_family_) { |
| StartRequest(); |
| } else { |
| LOG(ERROR) << logging_tag_ << ": Server hostname " << url_.host() |
| << " doesn't match the IP family " << ip_family_; |
| SendErrorAsync(Error::kDNSFailure); |
| } |
| return; |
| } |
| |
| for (const auto& dns : dns_list_) { |
| if (dns_queries_.size() > kDNSMaxParallelQueries) { |
| break; |
| } |
| auto cb = base::BindOnce(&HttpRequest::GetDNSResult, |
| weak_ptr_factory_.GetWeakPtr(), dns); |
| auto dns_options = dns_options_; |
| dns_options.name_server = dns; |
| dns_queries_[dns] = dns_client_factory_->Resolve( |
| ip_family_, url_.host(), std::move(cb), dns_options); |
| } |
| } |
| |
| void HttpRequest::StartRequest() { |
| std::string url_string = url_.ToString(); |
| SLOG(this, 2) << logging_tag_ << ": Starting request to " << url_string; |
| request_id_ = |
| brillo::http::Get(url_string, headers_, transport_, |
| base::BindOnce(&HttpRequest::SuccessCallback, |
| weak_ptr_factory_.GetWeakPtr()), |
| base::BindOnce(&HttpRequest::ErrorCallback, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void HttpRequest::SuccessCallback( |
| brillo::http::RequestID request_id, |
| std::unique_ptr<brillo::http::Response> response) { |
| if (request_id != request_id_) { |
| LOG(ERROR) << logging_tag_ << ": Expected request ID " << request_id_ |
| << " but got " << request_id; |
| SendError(Error::kInternalError); |
| return; |
| } |
| |
| auto callback = std::move(callback_); |
| Stop(); |
| |
| if (!callback.is_null()) { |
| std::move(callback).Run(std::move(response)); |
| } |
| } |
| |
| void HttpRequest::ErrorCallback(brillo::http::RequestID request_id, |
| const brillo::Error* error) { |
| int error_code; |
| if (error->GetDomain() != kCurlEasyError) { |
| LOG(ERROR) << logging_tag_ << ": Expected error domain " << kCurlEasyError |
| << " but got " << error->GetDomain(); |
| SendError(Error::kInternalError); |
| return; |
| } |
| if (request_id != request_id_) { |
| LOG(ERROR) << logging_tag_ << ": Expected request ID " << request_id_ |
| << " but got " << request_id; |
| SendError(Error::kInternalError); |
| return; |
| } |
| if (!base::StringToInt(error->GetCode(), &error_code)) { |
| LOG(ERROR) << logging_tag_ << ": Unable to convert error code " |
| << error->GetCode() << " to Int"; |
| SendError(Error::kInternalError); |
| return; |
| } |
| |
| // TODO(matthewmwang): This breaks abstraction. Modify brillo::http::Transport |
| // to provide an implementation agnostic error code. |
| switch (error_code) { |
| case CURLE_COULDNT_CONNECT: |
| SendError(Error::kConnectionFailure); |
| break; |
| case CURLE_PEER_FAILED_VERIFICATION: |
| SendError(Error::kTLSFailure); |
| break; |
| case CURLE_WRITE_ERROR: |
| case CURLE_READ_ERROR: |
| SendError(Error::kIOError); |
| break; |
| case CURLE_OPERATION_TIMEDOUT: |
| SendError(Error::kHTTPTimeout); |
| break; |
| default: |
| LOG(ERROR) << logging_tag_ << ": Unknown curl error code " << error_code; |
| SendError(Error::kInternalError); |
| } |
| } |
| |
| void HttpRequest::Stop() { |
| if (!is_running_) { |
| return; |
| } |
| dns_queries_.clear(); |
| is_running_ = false; |
| request_id_ = -1; |
| callback_.Reset(); |
| } |
| |
| // DnsClient callback that fires when the DNS request completes. |
| void HttpRequest::GetDNSResult(net_base::IPAddress dns, |
| base::TimeDelta duration, |
| const net_base::DNSClient::Result& result) { |
| if (!result.has_value()) { |
| LOG(WARNING) << logging_tag_ << ": Could not resolve " << url_.host() |
| << " with " << dns << ": " << result.error(); |
| auto error = Error::kDNSFailure; |
| if (result.error() == net_base::DNSClient::Error::kTimedOut) { |
| error = Error::kDNSTimeout; |
| } |
| dns_queries_.erase(dns); |
| if (dns_queries_.empty()) { |
| SendError(error); |
| } |
| return; |
| } |
| |
| // Cancel all other queries. |
| dns_queries_.clear(); |
| |
| // CURLOPT_RESOLVE expects the format "[+]HOST:PORT:ADDRESS[,ADDRESS]" for DNS |
| // cache entries, and brillo::http::Transport::ResolveHostToIp() already adds |
| // "HOST:PORT:". |
| std::string addresses; |
| std::string sep = ""; |
| for (const auto& addr : *result) { |
| base::StrAppend(&addresses, {sep, addr.ToString()}); |
| sep = ","; |
| } |
| // Add the host/port to IP mapping to the DNS cache to force curl to resolve |
| // the URL to the given IP. Otherwise, curl will do its own DNS resolution. |
| transport_->ResolveHostToIp(url_.host(), url_.port(), addresses); |
| LOG(INFO) << logging_tag_ << ": Resolved " << url_.host() << " to " |
| << addresses << " in " << duration; |
| StartRequest(); |
| } |
| |
| void HttpRequest::SendError(Error error) { |
| // Save copies on the stack, since Stop() will remove them. |
| auto callback = std::move(callback_); |
| Stop(); |
| // Call the callback last, since it may delete us and |this| may no longer |
| // be valid. |
| if (!callback.is_null()) { |
| std::move(callback).Run(base::unexpected(error)); |
| } |
| } |
| |
| void HttpRequest::SendErrorAsync(Error error) { |
| dispatcher_->PostTask(FROM_HERE, |
| base::BindOnce(&HttpRequest::SendError, |
| weak_ptr_factory_.GetWeakPtr(), error)); |
| } |
| |
| // static |
| std::string_view ErrorName(HttpRequest::Error error) { |
| switch (error) { |
| case HttpRequest::Error::kInternalError: |
| return "Internal error"; |
| case HttpRequest::Error::kDNSFailure: |
| return "DNS failure"; |
| case HttpRequest::Error::kDNSTimeout: |
| return "DNS timeout"; |
| case HttpRequest::Error::kConnectionFailure: |
| return "Connection failure"; |
| case HttpRequest::Error::kTLSFailure: |
| return "TLS failure"; |
| case HttpRequest::Error::kIOError: |
| return "IO error"; |
| case HttpRequest::Error::kHTTPTimeout: |
| return "Request timeout"; |
| } |
| } |
| |
| std::ostream& operator<<(std::ostream& stream, HttpRequest::Error error) { |
| return stream << ErrorName(error); |
| } |
| |
| std::ostream& operator<<(std::ostream& stream, |
| std::optional<HttpRequest::Error> error) { |
| return stream << (error ? ErrorName(*error) : "Success"); |
| } |
| |
| } // namespace shill |