1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
|
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_SUPERVISED_USER_CORE_BROWSER_PROTO_FETCHER_H_
#define COMPONENTS_SUPERVISED_USER_CORE_BROWSER_PROTO_FETCHER_H_
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ref.h"
#include "base/memory/scoped_refptr.h"
#include "base/types/expected.h"
#include "base/version_info/channel.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/supervised_user/core/browser/api_access_token_fetcher.h"
#include "components/supervised_user/core/browser/fetcher_config.h"
#include "components/supervised_user/core/browser/proto_fetcher_metrics.h"
#include "components/supervised_user/core/browser/proto_fetcher_status.h"
#include "components/supervised_user/core/common/supervised_user_constants.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "net/base/backoff_entry.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"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "third_party/protobuf/src/google/protobuf/message_lite.h"
namespace supervised_user {
// -----------------------------------------------------------------------------
// Usage documentation
// -----------------------------------------------------------------------------
//
// Overview: ProtoFetcher provides an interface for generic fetchers that
// use classes to represent Request and Response objects. The default mechanism
// under the hood takes care of the fetch process, including:
// * obtaining the right access token,
// * serializing the request and parsing the response,
// * submitting metrics.
//
// If you want to create new fetcher factory method, then some
// details must be provided in order to enable fetching for said Response. The
// new fetcher factory should have at least the following arguments:
// signin::IdentityManager, network::SharedURLLoaderFactory, consuming callback
// and must reference a static configuration.
//
// The static configuration should be placed in the fetcher_config.h module.
// Uses network::SharedURLLoaderFactory to issue network requests.
// Internally, it's a two-phase process: first the access token is fetched, and
// if applicable, the remote service is called and the response is processed.
// This abstract class doesn't make any assumptions on the request nor response
// formats and uses them as bare strings.
class FetchProcess {
public:
// Data to send. request_body and query_string are respective parts of the
// HTTP request.
struct Payload {
std::string request_body;
std::string query_string;
};
FetchProcess() = delete;
// Identity manager and fetcher_config must outlive this call.
FetchProcess(
signin::IdentityManager& identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const Payload& payload,
const FetcherConfig& fetcher_config,
const FetcherConfig::PathArgs& args = {},
std::optional<version_info::Channel> channel = std::nullopt);
// Not copyable.
FetchProcess(const FetchProcess&) = delete;
FetchProcess& operator=(const FetchProcess&) = delete;
virtual ~FetchProcess();
protected:
void RecordMetrics(const ProtoFetcherStatus& status) const;
private:
// First phase of fetching: the access token response is ready. Access token
// step is optional - empty access token means that access token procedure was
// not performed at all.
void OnAccessTokenFetchComplete(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
base::expected<signin::AccessTokenInfo, GoogleServiceAuthError>
access_token);
// Second phase of fetching: perform the request to the remote service. Access
// token is optional.
void StartUrlLoader(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const std::optional<signin::AccessTokenInfo> access_token_info);
// Third phase of fetching: the remote service responded
void OnSimpleUrlLoaderComplete(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
std::optional<std::string> response_body);
// Final phase of fetching: binary data is collected and ready to be
// interpreted or error is encountered.
virtual void OnResponse(std::optional<std::string> response_body) = 0;
virtual void OnError(const ProtoFetcherStatus& status) = 0;
const raw_ref<signin::IdentityManager> identity_manager_;
const Payload payload_;
const raw_ref<const FetcherConfig> config_;
const FetcherConfig::PathArgs args_;
std::optional<version_info::Channel> channel_;
std::optional<ProtoFetcherMetrics> metrics_;
// Entrypoint of the fetch process with end-user-credentials, which starts
// with ApiAccessToken access followed by a request made with SimpleURLLoader.
std::unique_ptr<ApiAccessTokenFetcher> fetcher_;
// Alternative entrypoint of the fetch process without end-user-credentials,
// or next stage when end-user-credentials are resolved.
std::unique_ptr<network::SimpleURLLoader> simple_url_loader_;
// If an auth error was encountered when fetching the access token, it is
// stored here (whether or not it was fatal).
std::optional<GoogleServiceAuthError> access_token_auth_error_;
// Whether we have triggered a retry following an HTTP auth error.
// This is tracked to avoid retry loops.
bool triggered_retry_on_http_auth_error_ = false;
};
// Overlay over FetchProcess that interprets successful responses as given
// Response type parameter.
// Use instance of TypedFetchProcess to start request and write the result onto
// the receiving delegate. Every instance of Fetcher is disposable and should be
// used only once.
template <typename Response>
class TypedFetchProcess : public FetchProcess {
public:
// Called when fetch completes. The response contains value iff the status
// doesn't signal error (see ProtoFetcherStatus::IsOK). In not-OK situations,
// the response is empty.
using Callback = base::OnceCallback<void(const ProtoFetcherStatus&,
std::unique_ptr<Response>)>;
TypedFetchProcess() = delete;
TypedFetchProcess(
signin::IdentityManager& identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const Payload& payload,
Callback callback,
const FetcherConfig& fetcher_config,
const FetcherConfig::PathArgs& args,
std::optional<version_info::Channel> channel)
: FetchProcess(identity_manager,
url_loader_factory,
payload,
fetcher_config,
args,
channel),
callback_(std::move(callback)) {}
~TypedFetchProcess() override = default;
private:
void OnResponse(std::optional<std::string> response_body) override {
CHECK(response_body) << "Use OnError when there is no response.";
std::unique_ptr<Response> response = std::make_unique<Response>();
if (!response->ParseFromString(*response_body)) {
OnError(ProtoFetcherStatus::InvalidResponse());
return;
}
OnSuccess(std::move(response));
}
void OnSuccess(std::unique_ptr<Response> response) {
CHECK(response) << "ProtoFetcherStatus::Ok implies non-empty response "
"(which is always a valid message).";
RecordMetrics(ProtoFetcherStatus::Ok());
std::move(callback_).Run(ProtoFetcherStatus::Ok(), std::move(response));
}
void OnError(const ProtoFetcherStatus& status) override {
RecordMetrics(status);
std::move(callback_).Run(status, nullptr);
}
Callback callback_;
};
// Proto fetcher owns the fetch process(es). Depending on the requested
// configuration, there might be multiple processes within one fetch.
template <typename Response>
class ProtoFetcher final {
public:
using Callback = TypedFetchProcess<Response>::Callback;
ProtoFetcher() = delete;
ProtoFetcher(
signin::IdentityManager& identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const FetchProcess::Payload& payload,
TypedFetchProcess<Response>::Callback callback,
const FetcherConfig& fetcher_config,
FetcherConfig::PathArgs args,
std::optional<version_info::Channel> channel)
: callback_(std::move(callback)),
factory_(base::BindRepeating(&ProtoFetcher<Response>::Factory,
base::Unretained(this),
std::ref(identity_manager),
url_loader_factory,
payload,
fetcher_config,
args,
channel)),
backoff_entry_(fetcher_config.BackoffEntry()),
metrics_(CumulativeProtoFetcherMetrics::FromConfig(fetcher_config)) {
Fetch();
}
// Not copyable.
ProtoFetcher(const ProtoFetcher&) = delete;
ProtoFetcher& operator=(const ProtoFetcher&) = delete;
private:
std::unique_ptr<TypedFetchProcess<Response>> Factory(
signin::IdentityManager& identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
FetchProcess::Payload payload,
const FetcherConfig& fetcher_config,
FetcherConfig::PathArgs args,
std::optional<version_info::Channel> channel) {
return std::make_unique<TypedFetchProcess<Response>>(
identity_manager, url_loader_factory, payload,
base::BindOnce(&ProtoFetcher<Response>::OnResponse,
base::Unretained(this)),
fetcher_config, args, channel);
}
void Stop() {
fetcher_.reset();
timer_.Stop();
}
void Fetch() {
retry_count_++;
fetcher_ = factory_.Run();
}
bool HasRetrySupportEnabled() const {
return static_cast<bool>(backoff_entry_);
}
bool ShouldRetry(const ProtoFetcherStatus& status) {
return HasRetrySupportEnabled() && status.IsTransientError();
}
void OnResponse(const ProtoFetcherStatus& status,
std::unique_ptr<Response> response) {
if (ShouldRetry(status)) {
Stop();
backoff_entry_->InformOfRequest(/*succeeded=*/false);
timer_.Start(FROM_HERE, backoff_entry_->GetTimeUntilRelease(), this,
&ProtoFetcher<Response>::Fetch);
return;
}
CHECK(callback_) << "Callback can be used only once.";
if (HasRetrySupportEnabled()) {
backoff_entry_->InformOfRequest(/*succeeded=*/true);
if (IsMetricsRecordingEnabled()) {
metrics_->RecordMetrics(status);
metrics_->RecordRetryCount(retry_count_);
}
}
std::move(callback_).Run(status, std::move(response));
}
inline bool IsMetricsRecordingEnabled() const { return metrics_.has_value(); }
// Client callback.
TypedFetchProcess<Response>::Callback callback_;
base::RepeatingCallback<std::unique_ptr<TypedFetchProcess<Response>>(void)>
factory_;
std::unique_ptr<TypedFetchProcess<Response>> fetcher_;
// Retry controls.
base::OneShotTimer timer_;
std::unique_ptr<net::BackoffEntry> backoff_entry_;
int retry_count_{0};
const std::optional<CumulativeProtoFetcherMetrics> metrics_;
};
// Tells if the FetcherConfig allows requests without end user credentials at
// any stage.
bool ConfiguresFetcherWithoutEndUserCredentials(
const FetcherConfig& fetcher_config);
// Constructs a launched fetcher. The fetcher will be either one shot or
// retryable, depending on the FetcherConfig::backoff_policy setting.
// `identity_manager` and `fetcher_config` must outlive this call.
//
// `args` are only relevant if `fetcher_config` uses template path (see
// supervised_user::FetcherConfig::service_path).
//
// `channel` must be specified if `fetcher_config` has
// `CredentialsRequirement::kBestEffort`.
template <typename Response>
std::unique_ptr<ProtoFetcher<Response>> CreateFetcher(
signin::IdentityManager& identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const FetchProcess::Payload& payload,
typename ProtoFetcher<Response>::Callback callback,
const FetcherConfig& fetcher_config,
const FetcherConfig::PathArgs& args = {},
const std::optional<version_info::Channel> channel = std::nullopt) {
CHECK(!ConfiguresFetcherWithoutEndUserCredentials(fetcher_config) || channel)
<< "The Chrome channel must be specified for fetchers which can send "
"requests without user credentials.";
return std::make_unique<ProtoFetcher<Response>>(
identity_manager, url_loader_factory, payload, std::move(callback),
fetcher_config, args, channel);
}
// Same as above, but payload is implicitly constructed from the request
template <typename Response>
std::unique_ptr<ProtoFetcher<Response>> CreateFetcher(
signin::IdentityManager& identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
const google::protobuf::MessageLite& message,
typename ProtoFetcher<Response>::Callback callback,
const FetcherConfig& fetcher_config,
const FetcherConfig::PathArgs& args = {},
const std::optional<version_info::Channel> channel = std::nullopt) {
return CreateFetcher<Response>(identity_manager, url_loader_factory,
{.request_body = message.SerializeAsString()},
std::move(callback), fetcher_config, args,
channel);
}
} // namespace supervised_user
#endif // COMPONENTS_SUPERVISED_USER_CORE_BROWSER_PROTO_FETCHER_H_
|