| // Copyright 2018 The Chromium OS 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 "shill/portal_detector.h" |
| |
| #include <string> |
| |
| #include <base/bind.h> |
| #include <base/rand_util.h> |
| #include <base/strings/pattern.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_util.h> |
| #include <base/strings/stringprintf.h> |
| #include <chromeos/dbus/service_constants.h> |
| |
| #include "shill/connection.h" |
| #include "shill/dns_client.h" |
| #include "shill/event_dispatcher.h" |
| #include "shill/logging.h" |
| #include "shill/metrics.h" |
| #include "shill/net/ip_address.h" |
| |
| using base::Bind; |
| using base::Callback; |
| using base::StringPrintf; |
| using std::string; |
| |
| namespace shill { |
| |
| namespace Logging { |
| static auto kModuleLogScope = ScopeLogger::kPortal; |
| static string ObjectID(Connection* c) { |
| return c->interface_name(); |
| } |
| } // namespace Logging |
| |
| const int PortalDetector::kInitialCheckIntervalSeconds = 3; |
| const int PortalDetector::kMaxPortalCheckIntervalSeconds = 5 * 60; |
| const char PortalDetector::kDefaultCheckPortalList[] = "ethernet,wifi,cellular"; |
| |
| const int PortalDetector::kRequestTimeoutSeconds = 10; |
| |
| const char PortalDetector::kDefaultHttpUrl[] = |
| "http://www.gstatic.com/generate_204"; |
| const char PortalDetector::kDefaultHttpsUrl[] = |
| "https://www.google.com/generate_204"; |
| const std::vector<string> PortalDetector::kDefaultFallbackHttpUrls{ |
| "http://www.google.com/gen_204", |
| "http://play.googleapis.com/generate_204", |
| }; |
| |
| PortalDetector::PortalDetector( |
| ConnectionRefPtr connection, |
| EventDispatcher* dispatcher, |
| Metrics* metrics, |
| const Callback<void(const PortalDetector::Result&, |
| const PortalDetector::Result&)>& callback) |
| : attempt_count_(0), |
| attempt_start_time_((struct timeval){0}), |
| connection_(connection), |
| dispatcher_(dispatcher), |
| metrics_(metrics), |
| weak_ptr_factory_(this), |
| portal_result_callback_(callback), |
| time_(Time::GetInstance()), |
| is_active_(false) {} |
| |
| PortalDetector::~PortalDetector() { |
| Stop(); |
| } |
| |
| bool PortalDetector::StartAfterDelay(const PortalDetector::Properties& props, |
| int delay_seconds) { |
| SLOG(connection_.get(), 3) << "In " << __func__; |
| |
| if (!StartTrial(props, delay_seconds * 1000)) { |
| return false; |
| } |
| // The attempt_start_time_ is calculated based on the current time and |
| // |delay_seconds|. This is used to determine if a portal detection attempt |
| // is in progress. |
| UpdateAttemptTime(delay_seconds); |
| return true; |
| } |
| |
| const string PortalDetector::PickHttpProbeUrl(const Properties& props) { |
| if (attempt_count_ == 0 || props.fallback_http_url_strings.empty()) { |
| return props.http_url_string; |
| } |
| return props.fallback_http_url_strings[base::RandInt( |
| 0, props.fallback_http_url_strings.size() - 1)]; |
| } |
| |
| bool PortalDetector::StartTrial(const Properties& props, |
| int start_delay_milliseconds) { |
| SLOG(connection_.get(), 3) << "In " << __func__; |
| |
| // This step is rerun on each attempt, but trying it here will allow |
| // Start() to abort on any obviously malformed URL strings. |
| HttpUrl http_url, https_url; |
| http_url_string_ = PickHttpProbeUrl(props); |
| https_url_string_ = props.https_url_string; |
| if (!http_url.ParseFromString(http_url_string_)) { |
| LOG(ERROR) << "Failed to parse URL string: " << props.http_url_string; |
| return false; |
| } |
| if (!https_url.ParseFromString(https_url_string_)) { |
| LOG(ERROR) << "Failed to parse URL string: " << props.https_url_string; |
| return false; |
| } |
| |
| if (http_request_ || https_request_) { |
| CleanupTrial(); |
| } else { |
| http_request_ = std::make_unique<HttpRequest>(connection_, dispatcher_); |
| // For non-default URLs, allow for secure communication with both Google and |
| // non-Google servers. |
| bool allow_non_google_https = (https_url_string_ != kDefaultHttpsUrl); |
| https_request_ = std::make_unique<HttpRequest>(connection_, dispatcher_, |
| allow_non_google_https); |
| } |
| StartTrialAfterDelay(start_delay_milliseconds); |
| attempt_count_++; |
| return true; |
| } |
| |
| void PortalDetector::StartTrialAfterDelay(int start_delay_milliseconds) { |
| SLOG(connection_.get(), 4) |
| << "In " << __func__ << " delay = " << start_delay_milliseconds << "ms."; |
| trial_.Reset( |
| Bind(&PortalDetector::StartTrialTask, weak_ptr_factory_.GetWeakPtr())); |
| dispatcher_->PostDelayedTask(FROM_HERE, trial_.callback(), |
| start_delay_milliseconds); |
| } |
| |
| void PortalDetector::StartTrialTask() { |
| base::Callback<void(std::shared_ptr<brillo::http::Response>)> |
| http_request_success_callback( |
| Bind(&PortalDetector::HttpRequestSuccessCallback, |
| weak_ptr_factory_.GetWeakPtr())); |
| base::Callback<void(HttpRequest::Result)> http_request_error_callback( |
| Bind(&PortalDetector::HttpRequestErrorCallback, |
| weak_ptr_factory_.GetWeakPtr())); |
| HttpRequest::Result http_result = |
| http_request_->Start(http_url_string_, http_request_success_callback, |
| http_request_error_callback); |
| if (http_result != HttpRequest::kResultInProgress) { |
| // Return successful HTTPS probe by default. |
| CompleteTrial(PortalDetector::GetPortalResultForRequestResult(http_result), |
| Result(Phase::kContent, Status::kFailure)); |
| return; |
| } |
| |
| base::Callback<void(std::shared_ptr<brillo::http::Response>)> |
| https_request_success_callback( |
| Bind(&PortalDetector::HttpsRequestSuccessCallback, |
| weak_ptr_factory_.GetWeakPtr())); |
| base::Callback<void(HttpRequest::Result)> https_request_error_callback( |
| Bind(&PortalDetector::HttpsRequestErrorCallback, |
| weak_ptr_factory_.GetWeakPtr())); |
| HttpRequest::Result https_result = |
| https_request_->Start(https_url_string_, https_request_success_callback, |
| https_request_error_callback); |
| if (https_result != HttpRequest::kResultInProgress) { |
| https_result_ = |
| std::make_unique<Result>(GetPortalResultForRequestResult(https_result)); |
| LOG(ERROR) << connection_->interface_name() |
| << StringPrintf( |
| " HTTPS probe start failed phase==%s, status==%s, " |
| "attempt count==%d", |
| PhaseToString(https_result_->phase).c_str(), |
| StatusToString(https_result_->status).c_str(), |
| attempt_count_); |
| } |
| is_active_ = true; |
| |
| trial_timeout_.Reset( |
| Bind(&PortalDetector::TimeoutTrialTask, weak_ptr_factory_.GetWeakPtr())); |
| dispatcher_->PostDelayedTask(FROM_HERE, trial_timeout_.callback(), |
| kRequestTimeoutSeconds * 1000); |
| } |
| |
| bool PortalDetector::IsActive() { |
| return is_active_; |
| } |
| |
| void PortalDetector::CompleteTrial(Result http_result, Result https_result) { |
| SLOG(connection_.get(), 3) << StringPrintf( |
| "Trial completed. HTTP probe finished with phase==%s, status==%s, HTTPS " |
| "probe finished with phase==%s, status==%s, attempt count==%d.", |
| PhaseToString(http_result.phase).c_str(), |
| StatusToString(http_result.status).c_str(), |
| PhaseToString(https_result.phase).c_str(), |
| StatusToString(https_result.status).c_str(), attempt_count_); |
| CompleteAttempt(http_result, https_result); |
| } |
| |
| void PortalDetector::CleanupTrial() { |
| trial_timeout_.Cancel(); |
| |
| http_result_.reset(); |
| https_result_.reset(); |
| if (http_request_) |
| http_request_->Stop(); |
| if (https_request_) |
| https_request_->Stop(); |
| |
| is_active_ = false; |
| } |
| |
| void PortalDetector::TimeoutTrialTask() { |
| LOG(ERROR) << connection_->interface_name() |
| << " Trial request timed out, attempt count==" << attempt_count_; |
| CompleteTrial(Result(Phase::kUnknown, Status::kTimeout), |
| Result(Phase::kUnknown, Status::kTimeout)); |
| } |
| |
| void PortalDetector::Stop() { |
| SLOG(connection_.get(), 3) << "In " << __func__; |
| |
| attempt_count_ = 0; |
| if (!http_request_ && !https_request_) |
| return; |
| |
| CleanupTrial(); |
| http_request_.reset(); |
| https_request_.reset(); |
| } |
| |
| void PortalDetector::CompleteRequest() { |
| if (https_result_ && http_result_) { |
| metrics_->NotifyPortalDetectionMultiProbeResult(*http_result_, |
| *https_result_); |
| CompleteTrial(*http_result_.get(), *https_result_.get()); |
| } |
| } |
| |
| void PortalDetector::HttpRequestSuccessCallback( |
| std::shared_ptr<brillo::http::Response> response) { |
| // TODO(matthewmwang): check for 0 length data as well |
| int status_code = response->GetStatusCode(); |
| if (status_code == brillo::http::status_code::NoContent) { |
| http_result_ = std::make_unique<Result>(Phase::kContent, Status::kSuccess); |
| } else if (status_code == brillo::http::status_code::Redirect) { |
| http_result_ = std::make_unique<Result>(Phase::kContent, Status::kRedirect); |
| string redirect_url_string = |
| response->GetHeader(brillo::http::response_header::kLocation); |
| if (redirect_url_string.empty()) { |
| LOG(ERROR) << "No Location field in redirect header."; |
| } else { |
| HttpUrl redirect_url; |
| if (!redirect_url.ParseFromString(redirect_url_string)) { |
| LOG(ERROR) << "Unable to parse redirect URL: " << redirect_url_string; |
| http_result_->status = Status::kFailure; |
| } else { |
| http_result_->redirect_url_string = redirect_url_string; |
| http_result_->probe_url_string = http_url_string_; |
| } |
| } |
| } else { |
| http_result_ = std::make_unique<Result>(Phase::kContent, Status::kFailure); |
| } |
| CompleteRequest(); |
| } |
| |
| void PortalDetector::HttpsRequestSuccessCallback( |
| std::shared_ptr<brillo::http::Response> response) { |
| int status_code = response->GetStatusCode(); |
| if (status_code == brillo::http::status_code::NoContent) { |
| // HTTPS probe success, probably no portal |
| https_result_ = std::make_unique<Result>(Phase::kContent, Status::kSuccess); |
| LOG(INFO) << connection_->interface_name() |
| << " HTTPS probe succeeded, probably no portal, attempt count==" |
| << attempt_count_; |
| } else { |
| // HTTPS probe didn't get 204, inconclusive |
| https_result_ = std::make_unique<Result>(Phase::kContent, Status::kFailure); |
| LOG(ERROR) << connection_->interface_name() |
| << " HTTPS probe returned with status code " << status_code |
| << ". Portal detection inconclusive, attempt count==" |
| << attempt_count_; |
| } |
| CompleteRequest(); |
| } |
| |
| void PortalDetector::HttpRequestErrorCallback(HttpRequest::Result result) { |
| http_result_ = |
| std::make_unique<Result>(GetPortalResultForRequestResult(result)); |
| CompleteRequest(); |
| } |
| |
| void PortalDetector::HttpsRequestErrorCallback(HttpRequest::Result result) { |
| https_result_ = |
| std::make_unique<Result>(GetPortalResultForRequestResult(result)); |
| LOG(INFO) << connection_->interface_name() |
| << " HTTPS probe failed with phase==" |
| << PortalDetector::PhaseToString(https_result_.get()->phase) |
| << ", status==" |
| << PortalDetector::StatusToString(https_result_.get()->status); |
| CompleteRequest(); |
| } |
| |
| bool PortalDetector::IsInProgress() { |
| return is_active_; |
| } |
| |
| void PortalDetector::CompleteAttempt(Result http_result, Result https_result) { |
| LOG(INFO) << connection_->interface_name() |
| << StringPrintf( |
| " Portal detection completed attempt %d with " |
| "phase==%s, status==%s", |
| attempt_count_, |
| PortalDetector::PhaseToString(http_result.phase).c_str(), |
| PortalDetector::StatusToString(http_result.status).c_str()); |
| |
| http_result.num_attempts = attempt_count_; |
| CleanupTrial(); |
| portal_result_callback_.Run(http_result, https_result); |
| } |
| |
| void PortalDetector::UpdateAttemptTime(int delay_seconds) { |
| time_->GetTimeMonotonic(&attempt_start_time_); |
| struct timeval delay_timeval = {delay_seconds, 0}; |
| timeradd(&attempt_start_time_, &delay_timeval, &attempt_start_time_); |
| } |
| |
| int PortalDetector::AdjustStartDelay(int init_delay_seconds) { |
| int next_attempt_delay_seconds = 0; |
| if (attempt_count_ > 0) { |
| struct timeval now, elapsed_time; |
| time_->GetTimeMonotonic(&now); |
| timersub(&now, &attempt_start_time_, &elapsed_time); |
| SLOG(connection_.get(), 4) << "Elapsed time from previous attempt is " |
| << elapsed_time.tv_sec << " seconds."; |
| if (elapsed_time.tv_sec < init_delay_seconds) { |
| next_attempt_delay_seconds = init_delay_seconds - elapsed_time.tv_sec; |
| } |
| } else { |
| LOG(FATAL) << "AdjustStartDelay in PortalDetector called without " |
| "previous attempts"; |
| } |
| SLOG(connection_.get(), 3) |
| << "Adjusting trial start delay from " << init_delay_seconds |
| << " seconds to " << next_attempt_delay_seconds << " seconds."; |
| return next_attempt_delay_seconds; |
| } |
| |
| // static |
| const string PortalDetector::PhaseToString(Phase phase) { |
| switch (phase) { |
| case Phase::kConnection: |
| return kPortalDetectionPhaseConnection; |
| case Phase::kDNS: |
| return kPortalDetectionPhaseDns; |
| case Phase::kHTTP: |
| return kPortalDetectionPhaseHttp; |
| case Phase::kContent: |
| return kPortalDetectionPhaseContent; |
| case Phase::kUnknown: |
| default: |
| return kPortalDetectionPhaseUnknown; |
| } |
| } |
| |
| // static |
| const string PortalDetector::StatusToString(Status status) { |
| switch (status) { |
| case Status::kSuccess: |
| return kPortalDetectionStatusSuccess; |
| case Status::kTimeout: |
| return kPortalDetectionStatusTimeout; |
| case Status::kRedirect: |
| return kPortalDetectionStatusRedirect; |
| case Status::kFailure: |
| default: |
| return kPortalDetectionStatusFailure; |
| } |
| } |
| |
| PortalDetector::Result PortalDetector::GetPortalResultForRequestResult( |
| HttpRequest::Result result) { |
| switch (result) { |
| case HttpRequest::kResultSuccess: |
| // The request completed without receiving the expected payload. |
| return Result(Phase::kContent, Status::kFailure); |
| case HttpRequest::kResultDNSFailure: |
| return Result(Phase::kDNS, Status::kFailure); |
| case HttpRequest::kResultDNSTimeout: |
| return Result(Phase::kDNS, Status::kTimeout); |
| case HttpRequest::kResultConnectionFailure: |
| return Result(Phase::kConnection, Status::kFailure); |
| case HttpRequest::kResultHTTPFailure: |
| return Result(Phase::kHTTP, Status::kFailure); |
| case HttpRequest::kResultHTTPTimeout: |
| return Result(Phase::kHTTP, Status::kTimeout); |
| case HttpRequest::kResultInvalidInput: |
| case HttpRequest::kResultUnknown: |
| default: |
| return Result(Phase::kUnknown, Status::kFailure); |
| } |
| } |
| |
| } // namespace shill |