blob: f635fc4d0b4789c7742cca157d10627135893d42 [file] [log] [blame]
// Copyright 2020 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 "components/permissions/prediction_service/prediction_service.h"
#include <cmath>
#include <memory>
#include <utility>
#include "base/command_line.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/notreached.h"
#include "components/permissions/features.h"
#include "components/permissions/prediction_service/prediction_request_features.h"
#include "components/permissions/prediction_service/prediction_service_common.h"
#include "components/permissions/prediction_service/prediction_service_messages.pb.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/http/http_status_code.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/fetch_api.mojom-shared.h"
namespace {
constexpr base::TimeDelta kURLLookupTimeout = base::TimeDelta::FromSeconds(2);
permissions::ClientFeatures_Gesture ConvertToProtoGesture(
const permissions::PermissionRequestGestureType type) {
switch (type) {
case permissions::PermissionRequestGestureType::GESTURE:
return permissions::ClientFeatures_Gesture_GESTURE;
case permissions::PermissionRequestGestureType::NO_GESTURE:
return permissions::ClientFeatures_Gesture_NO_GESTURE;
case permissions::PermissionRequestGestureType::UNKNOWN:
return permissions::ClientFeatures_Gesture_UNKNOWN_GESTURE;
case permissions::PermissionRequestGestureType::NUM:
break;
}
NOTREACHED();
return permissions::ClientFeatures_Gesture_UNKNOWN_GESTURE;
}
inline float GetRatioRoundedToTwoDecimals(int numerator, int denominator) {
if (denominator == 0)
return 0;
return roundf(100.f * numerator / denominator) / 100.f;
}
void FillInStatsFeatures(
const permissions::PredictionRequestFeatures::ActionCounts& counts,
permissions::StatsFeatures* features) {
int total_counts =
counts.denies + counts.dismissals + counts.grants + counts.ignores;
// Round to only 2 decimal places to help prevent fingerprinting.
features->set_avg_deny_rate(
GetRatioRoundedToTwoDecimals(counts.denies, total_counts));
features->set_avg_dismiss_rate(
GetRatioRoundedToTwoDecimals(counts.dismissals, total_counts));
features->set_avg_grant_rate(
GetRatioRoundedToTwoDecimals(counts.grants, total_counts));
features->set_avg_ignore_rate(
GetRatioRoundedToTwoDecimals(counts.ignores, total_counts));
// Prevent hyperspecific large counts from becoming usable to fingerprint
// users that see an unexpectedly large prompt count.
features->set_prompts_count(std::min(total_counts, 100));
}
net::NetworkTrafficAnnotationTag GetTrafficAnnotationTag() {
return net::DefineNetworkTrafficAnnotation("permission_predictions", R"(
semantics {
sender: "Web Permission Perdictions"
description:
"A request to the Web Permission Predictions Service. The service will "
"attempt to predict the likelihood that the user would grant this "
"permission. Based on this prediction Chrome might decide to present "
"the user with a different UI; a less intrusive one."
trigger:
"A permission prompt is about to be shown to the user, and the user "
"has opted into Safe Browsing's Enhanced Protection."
data:
"User stats helpful for attempting to predict the user's likelihood "
"of granting the permission: the permission type, the presence of a "
"user gesture, the user's OS, average deny/grant/ignore/dismiss rates "
"and total prompts shown, both for the specific permission type and "
"overall for all permission types."
destination: GOOGLE_OWNED_SERVICE
}
policy {
cookies_allowed: NO
setting:
"This can be disabled by disabling Enhanced Protection by going to "
"Settings and then to the Security sub-menu."
chrome_policy {
SafeBrowsingProtectionLevel {
SafeBrowsingProtectionLevel: 1
}
}
})");
}
} // namespace
namespace permissions {
PredictionService::PredictionService(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_(url_loader_factory) {}
PredictionService::~PredictionService() = default;
void PredictionService::StartLookup(const PredictionRequestFeatures& entity,
LookupRequestCallback request_callback,
LookupResponseCallback response_callback) {
auto request = GetResourceRequest();
auto proto_request = GetPredictionRequestProto(entity);
std::string request_data;
proto_request->SerializeToString(&request_data);
SendRequestInternal(std::move(request), request_data, entity,
std::move(response_callback));
if (request_callback)
std::move(request_callback).Run(std::move(proto_request), std::string());
}
// static
const GURL PredictionService::GetPredictionServiceUrl(
bool recalculate_for_testing) {
static base::NoDestructor<GURL> default_prediction_service_url{
kDefaultPredictionServiceUrl};
static base::NoDestructor<GURL> command_line_url_override{
base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
kDefaultPredictionServiceUrlSwitchKey)};
static base::NoDestructor<GURL> feature_param_url_override{
feature_params::kPermissionPredictionServiceUrlOverride.Get()};
// To facilitate tests that want to exercise various url building logic,
// reinitialize the static variables if this flag is set.
if (recalculate_for_testing) {
*command_line_url_override =
GURL(base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
kDefaultPredictionServiceUrlSwitchKey));
*feature_param_url_override =
GURL(feature_params::kPermissionPredictionServiceUrlOverride.Get());
}
if (command_line_url_override->is_valid())
return *command_line_url_override;
if (feature_param_url_override->is_valid())
return *feature_param_url_override;
return *default_prediction_service_url;
}
std::unique_ptr<network::ResourceRequest>
PredictionService::GetResourceRequest() {
auto request = std::make_unique<network::ResourceRequest>();
request->url =
prediction_service_url_override_.is_empty()
? GetPredictionServiceUrl(recalculate_service_url_every_time)
: prediction_service_url_override_;
request->load_flags = net::LOAD_DISABLE_CACHE;
request->method = "POST";
request->credentials_mode = network::mojom::CredentialsMode::kOmit;
return request;
}
std::unique_ptr<GetSuggestionsRequest>
PredictionService::GetPredictionRequestProto(
const PredictionRequestFeatures& entity) {
auto proto_request = std::make_unique<GetSuggestionsRequest>();
ClientFeatures* client_features = proto_request->mutable_client_features();
client_features->set_platform(GetCurrentPlatformProto());
client_features->set_gesture(ConvertToProtoGesture(entity.gesture));
FillInStatsFeatures(entity.all_permission_counts,
client_features->mutable_client_stats());
PermissionFeatures* permission_features =
proto_request->mutable_permission_features()->Add();
FillInStatsFeatures(entity.requested_permission_counts,
permission_features->mutable_permission_stats());
switch (entity.type) {
case RequestType::kNotifications:
permission_features->mutable_notification_permission()
->Clear();
break;
default:
NOTREACHED() << "CPSS only supports notifications at the moment.";
}
return proto_request;
}
void PredictionService::SendRequestInternal(
std::unique_ptr<network::ResourceRequest> request,
const std::string& request_data,
const PredictionRequestFeatures& entity,
LookupResponseCallback response_callback) {
std::unique_ptr<network::SimpleURLLoader> owned_loader =
network::SimpleURLLoader::Create(std::move(request),
GetTrafficAnnotationTag());
owned_loader->AttachStringForUpload(request_data, "application/x-protobuf");
owned_loader->SetTimeoutDuration(kURLLookupTimeout);
owned_loader->DownloadToString(
url_loader_factory_.get(),
base::BindOnce(&PredictionService::OnURLLoaderComplete,
weak_factory_.GetWeakPtr(), entity, owned_loader.get(),
base::TimeTicks::Now()),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
pending_requests_[std::move(owned_loader)] = std::move(response_callback);
}
void PredictionService::OnURLLoaderComplete(
const PredictionRequestFeatures& entity,
network::SimpleURLLoader* loader,
base::TimeTicks request_start_time,
std::unique_ptr<std::string> response_body) {
for (auto& request : pending_requests_) {
if (request.first.get() == loader) {
auto prediction_response =
CreatePredictionsResponse(loader, response_body.get());
if (request.second) {
std::move(request.second)
.Run(prediction_response != nullptr /* Lookup successful */,
false /* Response from cache */,
std::move(prediction_response));
}
pending_requests_.erase(request.first);
return;
}
}
NOTREACHED() << "Unexpected loader callback.";
}
std::unique_ptr<GetSuggestionsResponse>
PredictionService::CreatePredictionsResponse(network::SimpleURLLoader* loader,
const std::string* response_body) {
if (!response_body || loader->NetError() != net::OK ||
loader->ResponseInfo()->headers->response_code() != net::HTTP_OK) {
return nullptr;
}
auto predictions_response = std::make_unique<GetSuggestionsResponse>();
if (predictions_response->ParseFromString(*response_body)) {
return predictions_response;
}
return nullptr;
}
} // namespace permissions