blob: a50404ea1354018b64b635f4c6e25d6c06c8e8a1 [file] [log] [blame]
// 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 <base/bind.h>
#include <base/logging.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/dns_client.h"
#include "shill/event_dispatcher.h"
#include "shill/logging.h"
#include "shill/metrics.h"
namespace {
const char kLinuxUserAgent[] =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/89.0.4389.114 Safari/537.36";
const brillo::http::HeaderList kHeaders{
{brillo::http::request_header::kUserAgent, kLinuxUserAgent},
};
// Base time interval between two portal detection attempts. Should be doubled
// at every new attempt.
constexpr base::TimeDelta kPortalCheckInterval =
base::TimeDelta::FromSeconds(3);
// Max time interval between two portal detection attempts.
constexpr base::TimeDelta kMaxPortalCheckInterval =
base::TimeDelta::FromMinutes(5);
} // namespace
namespace shill {
namespace Logging {
static auto kModuleLogScope = ScopeLogger::kPortal;
static std::string ObjectID(const PortalDetector* pd) {
return pd->LoggingTag();
}
} // namespace Logging
const char PortalDetector::kDefaultCheckPortalList[] = "ethernet,wifi,cellular";
const char PortalDetector::kDefaultHttpUrl[] =
"http://www.gstatic.com/generate_204";
const char PortalDetector::kDefaultHttpsUrl[] =
"https://www.google.com/generate_204";
const std::vector<std::string> PortalDetector::kDefaultFallbackHttpUrls{
"http://www.google.com/gen_204",
"http://play.googleapis.com/generate_204",
"http://connectivitycheck.gstatic.com/generate_204",
};
PortalDetector::PortalDetector(EventDispatcher* dispatcher,
Metrics* metrics,
base::Callback<void(const Result&)> callback)
: attempt_count_(0),
last_attempt_start_time_(),
dispatcher_(dispatcher),
metrics_(metrics),
weak_ptr_factory_(this),
portal_result_callback_(callback),
is_active_(false) {}
PortalDetector::~PortalDetector() {
Stop();
}
const std::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::Start(const PortalDetector::Properties& props,
const std::string& ifname,
const IPAddress& src_address,
const std::vector<std::string>& dns_list,
base::TimeDelta delay) {
logging_tag_ =
ifname + " " + IPAddress::GetAddressFamilyName(src_address.family());
SLOG(this, 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) << LoggingTag() << ": Failed to parse HTTP probe URL string: "
<< props.http_url_string;
return false;
}
if (!https_url.ParseFromString(https_url_string_)) {
LOG(ERROR) << "Failed to parse HTTPS probe URL string: "
<< props.https_url_string;
return false;
}
attempt_count_++;
if (http_request_ || https_request_) {
CleanupTrial();
} else {
http_request_ =
std::make_unique<HttpRequest>(dispatcher_, LoggingTag() + " HTTP probe",
ifname, src_address, dns_list);
// 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>(
dispatcher_, LoggingTag() + " HTTPS probe", ifname, src_address,
dns_list, allow_non_google_https);
}
trial_.Reset(base::Bind(&PortalDetector::StartTrialTask,
weak_ptr_factory_.GetWeakPtr()));
dispatcher_->PostDelayedTask(FROM_HERE, trial_.callback(),
delay.InMilliseconds());
// |last_attempt_start_time_| is calculated based on the current time and
// |delay|. This is used to determine when to schedule the next portal
// detection attempt after this one.
last_attempt_start_time_ = base::Time::NowFromSystemTime() + delay;
return true;
}
void PortalDetector::StartTrialTask() {
LOG(INFO) << LoggingTag() << ": Starting trial";
base::Callback<void(std::shared_ptr<brillo::http::Response>)>
http_request_success_callback(
base::Bind(&PortalDetector::HttpRequestSuccessCallback,
weak_ptr_factory_.GetWeakPtr()));
base::Callback<void(HttpRequest::Result)> http_request_error_callback(
base::Bind(&PortalDetector::HttpRequestErrorCallback,
weak_ptr_factory_.GetWeakPtr()));
HttpRequest::Result http_result = http_request_->Start(
http_url_string_, kHeaders, http_request_success_callback,
http_request_error_callback);
if (http_result != HttpRequest::kResultInProgress) {
// If the http probe fails to start, complete the trial with a failure
// Result for https.
LOG(ERROR) << LoggingTag()
<< ": HTTP probe failed to start. Aborting trial.";
PortalDetector::Result result;
result.http_phase = GetPortalPhaseForRequestResult(http_result);
result.http_status = GetPortalStatusForRequestResult(http_result);
result.https_phase = PortalDetector::Phase::kContent;
result.https_status = PortalDetector::Status::kFailure;
CompleteTrial(result);
return;
}
result_ = std::make_unique<Result>();
base::Callback<void(std::shared_ptr<brillo::http::Response>)>
https_request_success_callback(
base::Bind(&PortalDetector::HttpsRequestSuccessCallback,
weak_ptr_factory_.GetWeakPtr()));
base::Callback<void(HttpRequest::Result)> https_request_error_callback(
base::Bind(&PortalDetector::HttpsRequestErrorCallback,
weak_ptr_factory_.GetWeakPtr()));
HttpRequest::Result https_result = https_request_->Start(
https_url_string_, kHeaders, https_request_success_callback,
https_request_error_callback);
if (https_result != HttpRequest::kResultInProgress) {
result_->https_phase = GetPortalPhaseForRequestResult(https_result);
result_->https_status = GetPortalStatusForRequestResult(https_result);
LOG(ERROR) << LoggingTag() << ": HTTPS probe failed to start";
// To find the portal sign-in url, wait for the HTTP probe to complete
// before completing the trial and calling |portal_result_callback_|.
}
is_active_ = true;
}
void PortalDetector::CompleteTrial(Result result) {
LOG(INFO) << LoggingTag()
<< ": Trial completed. HTTP probe: phase=" << result.http_phase
<< ", status=" << result.http_status
<< ". HTTPS probe: phase=" << result.https_phase
<< ", status=" << result.https_status;
result.num_attempts = attempt_count_;
metrics_->NotifyPortalDetectionMultiProbeResult(result);
CleanupTrial();
portal_result_callback_.Run(result);
}
void PortalDetector::CleanupTrial() {
result_.reset();
if (http_request_)
http_request_->Stop();
if (https_request_)
https_request_->Stop();
is_active_ = false;
}
void PortalDetector::Stop() {
SLOG(this, 3) << "In " << __func__;
attempt_count_ = 0;
if (!http_request_ && !https_request_)
return;
CleanupTrial();
http_request_.reset();
https_request_.reset();
}
void PortalDetector::HttpRequestSuccessCallback(
std::shared_ptr<brillo::http::Response> response) {
// TODO(matthewmwang): check for 0 length data as well
int status_code = response->GetStatusCode();
result_->http_probe_completed = true;
result_->http_phase = Phase::kContent;
result_->http_status_code = status_code;
if (status_code == brillo::http::status_code::NoContent) {
result_->http_status = Status::kSuccess;
} else if (status_code == brillo::http::status_code::Redirect) {
result_->http_status = Status::kRedirect;
std::string redirect_url_string =
response->GetHeader(brillo::http::response_header::kLocation);
if (redirect_url_string.empty()) {
LOG(ERROR) << LoggingTag() << ": No Location field in redirect header.";
} else {
HttpUrl redirect_url;
if (!redirect_url.ParseFromString(redirect_url_string)) {
LOG(ERROR) << LoggingTag()
<< ": Unable to parse redirect URL: " << redirect_url_string;
result_->http_status = Status::kFailure;
} else {
LOG(INFO) << LoggingTag() << ": Redirect URL: " << redirect_url_string;
result_->redirect_url_string = redirect_url_string;
result_->probe_url_string = http_url_string_;
}
}
} else {
result_->http_status = Status::kFailure;
}
LOG(INFO) << LoggingTag() << ": HTTP probe response code=" << status_code
<< " status=" << result_->http_status;
if (result_->IsComplete())
CompleteTrial(*result_);
}
void PortalDetector::HttpsRequestSuccessCallback(
std::shared_ptr<brillo::http::Response> response) {
int status_code = response->GetStatusCode();
// The HTTPS probe is successful and indicates no portal was present only if
// it gets the expected 204 status code. Any other result is a failure.
result_->https_probe_completed = true;
result_->https_phase = Phase::kContent;
result_->https_status = (status_code == brillo::http::status_code::NoContent)
? Status::kSuccess
: Status::kFailure;
LOG(INFO) << LoggingTag() << ": HTTPS probe response code=" << status_code
<< " status=" << result_->https_status;
if (result_->IsComplete())
CompleteTrial(*result_);
}
void PortalDetector::HttpRequestErrorCallback(HttpRequest::Result http_result) {
result_->http_probe_completed = true;
result_->http_phase = GetPortalPhaseForRequestResult(http_result);
result_->http_status = GetPortalStatusForRequestResult(http_result);
LOG(INFO) << LoggingTag()
<< ": HTTP probe failed with phase=" << result_->http_phase
<< " status=" << result_->http_status;
if (result_->IsComplete())
CompleteTrial(*result_);
}
void PortalDetector::HttpsRequestErrorCallback(
HttpRequest::Result https_result) {
result_->https_probe_completed = true;
result_->https_phase = GetPortalPhaseForRequestResult(https_result);
result_->https_status = GetPortalStatusForRequestResult(https_result);
LOG(INFO) << LoggingTag()
<< ": HTTPS probe failed with phase=" << result_->http_phase
<< " status=" << result_->http_status;
if (result_->IsComplete())
CompleteTrial(*result_);
}
bool PortalDetector::IsInProgress() {
return is_active_;
}
base::TimeDelta PortalDetector::GetNextAttemptDelay() {
if (attempt_count_ == 0)
return base::TimeDelta();
base::TimeDelta next_interval =
kPortalCheckInterval * (1 << (attempt_count_ - 1));
if (next_interval > kMaxPortalCheckInterval)
next_interval = kMaxPortalCheckInterval;
const auto next_attempt = last_attempt_start_time_ + next_interval;
const auto now = base::Time::NowFromSystemTime();
if (next_attempt < now)
return base::TimeDelta();
return next_attempt - now;
}
// static
const std::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 std::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;
}
}
// static
PortalDetector::Phase PortalDetector::GetPortalPhaseForRequestResult(
HttpRequest::Result result) {
switch (result) {
case HttpRequest::kResultSuccess:
return Phase::kContent;
case HttpRequest::kResultDNSFailure:
return Phase::kDNS;
case HttpRequest::kResultDNSTimeout:
return Phase::kDNS;
case HttpRequest::kResultConnectionFailure:
return Phase::kConnection;
case HttpRequest::kResultHTTPFailure:
return Phase::kHTTP;
case HttpRequest::kResultHTTPTimeout:
return Phase::kHTTP;
case HttpRequest::kResultInvalidInput:
case HttpRequest::kResultUnknown:
default:
return Phase::kUnknown;
}
}
// static
PortalDetector::Status PortalDetector::GetPortalStatusForRequestResult(
HttpRequest::Result result) {
switch (result) {
case HttpRequest::kResultSuccess:
// The request completed without receiving the expected payload.
return Status::kFailure;
case HttpRequest::kResultDNSFailure:
return Status::kFailure;
case HttpRequest::kResultDNSTimeout:
return Status::kTimeout;
case HttpRequest::kResultConnectionFailure:
return Status::kFailure;
case HttpRequest::kResultHTTPFailure:
return Status::kFailure;
case HttpRequest::kResultHTTPTimeout:
return Status::kTimeout;
case HttpRequest::kResultInvalidInput:
case HttpRequest::kResultUnknown:
default:
return Status::kFailure;
}
}
Service::ConnectState PortalDetector::Result::GetConnectionState() const {
if (http_phase != PortalDetector::Phase::kContent) {
return Service::kStateNoConnectivity;
}
if (http_status == PortalDetector::Status::kSuccess &&
https_status == PortalDetector::Status::kSuccess) {
return Service::kStateOnline;
}
if (http_status == PortalDetector::Status::kRedirect) {
return redirect_url_string.empty() ? Service::kStatePortalSuspected
: Service::kStateRedirectFound;
}
if (http_status == PortalDetector::Status::kTimeout &&
https_status != PortalDetector::Status::kSuccess) {
return Service::kStateNoConnectivity;
}
return Service::kStatePortalSuspected;
}
std::string PortalDetector::LoggingTag() const {
return logging_tag_ + " attempt=" + std::to_string(attempt_count_);
}
bool PortalDetector::Result::IsComplete() const {
return http_probe_completed && https_probe_completed;
}
std::ostream& operator<<(std::ostream& stream, PortalDetector::Phase phase) {
return stream << PortalDetector::PhaseToString(phase);
}
std::ostream& operator<<(std::ostream& stream, PortalDetector::Status status) {
return stream << PortalDetector::StatusToString(status);
}
} // namespace shill