// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chromeos/printing/printer_config_cache.h"
#include <memory>
#include <optional>
#include <string_view>
#include <utility>
#include <vector>
#include "base/containers/queue.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/clock.h"
#include "base/time/time.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "url/gurl.h"
namespace chromeos {
namespace {
// Defines the serving root in which all PPDs and PPD metadata reside.
constexpr char kServingRoot[] =
"https://printerconfigurations.googleusercontent.com/"
"chromeos_printing/";
constexpr char kLocalhostRoot[] = "http://localhost:7002/";
// Prepends the serving root to |name|, returning the result.
std::string PrependServingRoot(const std::string& name,
bool use_localhost_as_root) {
if (use_localhost_as_root) {
return base::StrCat({kLocalhostRoot, name});
}
return base::StrCat({kServingRoot, name});
}
// Accepts a relative |path| to a value in the Chrome OS Printing
// serving root) and returns a resource request to satisfy the same.
std::unique_ptr<network::ResourceRequest> FormRequest(
const std::string& path,
bool use_localhost_as_root) {
GURL full_url(PrependServingRoot(path, use_localhost_as_root));
if (!full_url.is_valid()) {
return nullptr;
}
auto request = std::make_unique<network::ResourceRequest>();
request->url = full_url;
request->load_flags = net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE;
request->credentials_mode = network::mojom::CredentialsMode::kOmit;
return request;
}
} // namespace
// In case of fetch failure, only the key is meaningful feedback.
// static
PrinterConfigCache::FetchResult PrinterConfigCache::FetchResult::Failure(
const std::string& key) {
return PrinterConfigCache::FetchResult{false, key, std::string(),
base::Time()};
}
// static
PrinterConfigCache::FetchResult PrinterConfigCache::FetchResult::Success(
const std::string& key,
const std::string& contents,
base::Time time_of_fetch) {
return PrinterConfigCache::FetchResult{true, key, contents, time_of_fetch};
}
class PrinterConfigCacheImpl : public PrinterConfigCache {
public:
explicit PrinterConfigCacheImpl(
const base::Clock* clock,
base::RepeatingCallback<network::mojom::URLLoaderFactory*()>
loader_factory_dispenser,
bool use_localhost_as_root)
: clock_(clock),
loader_factory_dispenser_(std::move(loader_factory_dispenser)),
use_localhost_as_root_(use_localhost_as_root),
weak_factory_(this) {}
~PrinterConfigCacheImpl() override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
void Fetch(const std::string& key,
base::TimeDelta expiration,
FetchCallback cb) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Try to answer this fetch request locally.
const auto& finding = cache_.find(key);
if (finding != cache_.end()) {
const Entry& entry = finding->second;
if (entry.time_of_fetch + expiration > clock_->Now()) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(cb), FetchResult::Success(
key, entry.contents,
entry.time_of_fetch)));
return;
}
}
// We couldn't answer this request locally. Issue a networked fetch
// and defer the answer to when we hear back.
auto context = std::make_unique<FetchContext>(key, std::move(cb));
fetch_queue_.push(std::move(context));
TryToStartNetworkedFetch();
}
void Drop(const std::string& key) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
cache_.erase(key);
}
private:
// A FetchContext saves off the key and the FetchCallback that a
// caller passes to PrinterConfigCacheImpl::Fetch().
struct FetchContext {
const std::string key;
PrinterConfigCache::FetchCallback cb;
FetchContext(const std::string& arg_key,
PrinterConfigCache::FetchCallback arg_cb)
: key(arg_key), cb(std::move(arg_cb)) {}
~FetchContext() = default;
};
// If a PrinterConfigCache maps keys to values, then Entry structs
// represent values.
struct Entry {
std::string contents;
base::Time time_of_fetch;
Entry(const std::string& arg_contents, base::Time time)
: contents(arg_contents), time_of_fetch(time) {}
~Entry() = default;
};
void TryToStartNetworkedFetch() {
// Either
// 1. a networked fetch is already in flight or
// 2. there are no more pending networked fetches to act upon.
// In either case, we can't do anything at the moment; back off
// and let a future call to Fetch() or FinishNetworkedFetch()
// return here to try again.
if (fetcher_ || fetch_queue_.empty()) {
return;
}
std::unique_ptr<FetchContext> context = std::move(fetch_queue_.front());
fetch_queue_.pop();
auto request = FormRequest(context->key, use_localhost_as_root_);
// Create traffic annotation tag.
net::NetworkTrafficAnnotationTag traffic_annotation =
net::DefineNetworkTrafficAnnotation("printer_config_fetch", R"(
semantics {
sender: "Printer Configuration"
description:
"This component sends requests to the Chrome OS Printing "
"serving root during printer configuration. This can return "
"two pieces of information, depending on the request: "
"PostScript Printer Description (PPD) files for a specified "
"printer, and PPD file metadata to help locate the desired PPD "
"file."
trigger: "On printer setup in ChromeOS."
data: "Printer names (comprising of make and/or model)."
user_data: {
type: OTHER
}
destination: GOOGLE_OWNED_SERVICE
internal: {
contacts: {
email: "[email protected]"
}
}
last_reviewed: "2023-01-18"
}
policy {
cookies_allowed: NO
setting:
"Admins must disable access to both enterprise and "
"non-enterprise printers. Enterprise printers should be left "
"empty under 'Devices > Chrome > Printers'. Non-enterprise "
"printers can be disabled under 'Devices > Chrome > Settings > "
"Printer management' by setting to: 'Do not allow users to add "
"new printers'."
chrome_policy {
UserPrintersAllowed {
UserPrintersAllowed: false
}
PrintersBulkConfiguration: {
PrintersBulkConfiguration: ""
}
}
chrome_device_policy {
# DevicePrinters
device_printers: {
external_policy: ""
}
}
})");
fetcher_ = network::SimpleURLLoader::Create(std::move(request),
traffic_annotation);
fetcher_->DownloadToString(
loader_factory_dispenser_.Run(),
base::BindOnce(&PrinterConfigCacheImpl::FinishNetworkedFetch,
weak_factory_.GetWeakPtr(), std::move(context)),
network::SimpleURLLoader::kMaxBoundedStringDownloadSize);
}
// Called by |fetcher_| once DownloadToString() completes.
void FinishNetworkedFetch(std::unique_ptr<FetchContext> context,
std::unique_ptr<std::string> contents) {
// Wherever |fetcher_| works its sorcery, it had better have posted
// back onto _our_ sequence.
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (fetcher_->NetError() == net::Error::OK) {
// We only want to update our local cache if the |fetcher_|
// succeeded; otherwise, prefer to either retain the stale entry
// (if extant) or retain no entry at all (if not).
const Entry newly_inserted = Entry(*contents, clock_->Now());
cache_.insert_or_assign(context->key, newly_inserted);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(context->cb),
FetchResult::Success(
context->key, newly_inserted.contents,
newly_inserted.time_of_fetch)));
} else {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(context->cb),
FetchResult::Failure(context->key)));
}
fetcher_.reset();
TryToStartNetworkedFetch();
}
// The heart of an PrinterConfigCache: the local cache itself.
base::flat_map<std::string, Entry> cache_;
// Enqueues networked requests.
base::queue<std::unique_ptr<FetchContext>> fetch_queue_;
// Dispenses Time objects to mark time of fetch on Entry instances.
raw_ptr<const base::Clock> clock_;
// Dispenses fresh URLLoaderFactory instances; see header comment
// on Create().
base::RepeatingCallback<network::mojom::URLLoaderFactory*()>
loader_factory_dispenser_;
// Talks to the networked service to fetch resources.
//
// Because this class is sequenced, a non-nullptr value here (observed
// on-sequence) denotes an ongoing fetch. See the
// TryToStartNetworkedFetch() and FinishNetworkedFetch() methods.
std::unique_ptr<network::SimpleURLLoader> fetcher_;
// Determines the address of the server.
const bool use_localhost_as_root_;
SEQUENCE_CHECKER(sequence_checker_);
// Dispenses weak pointers to our |fetcher_|. This is necessary
// because |this| could be deleted while the loader is in flight
// off-sequence.
base::WeakPtrFactory<PrinterConfigCacheImpl> weak_factory_;
};
// static
std::unique_ptr<PrinterConfigCache> PrinterConfigCache::Create(
const base::Clock* clock,
base::RepeatingCallback<network::mojom::URLLoaderFactory*()>
loader_factory_dispenser,
bool use_localhost_as_root) {
return std::make_unique<PrinterConfigCacheImpl>(
clock, std::move(loader_factory_dispenser), use_localhost_as_root);
}
} // namespace chromeos