blob: 3d1071a42f201dc402e6205a5449bd4743dd09f2 [file]
// Copyright 2016 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 "chromeos/printing/ppd_provider.h"
#include <utility>
#include "base/files/file_util.h"
#include "base/json/json_parser.h"
#include "base/memory/ptr_util.h"
#include "base/sequence_checker.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/task_runner_util.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chromeos/printing/ppd_cache.h"
#include "net/base/load_flags.h"
#include "net/http/http_status_code.h"
#include "net/url_request/url_fetcher.h"
#include "net/url_request/url_fetcher_delegate.h"
#include "net/url_request/url_request_context_getter.h"
#include "url/gurl.h"
namespace chromeos {
namespace printing {
namespace {
// Expected fields from the quirks server.
const char kJSONPPDKey[] = "compressedPpd";
const char kJSONLastUpdatedKey[] = "lastUpdatedTime";
const char kJSONTopListKey[] = "manufacturers";
const char kJSONManufacturer[] = "manufacturer";
const char kJSONModelList[] = "models";
class PpdProviderImpl;
// URLFetcherDelegate that just invokes a callback when the fetch is complete.
class ForwardingURLFetcherDelegate : public net::URLFetcherDelegate {
public:
explicit ForwardingURLFetcherDelegate(
const base::Callback<void()>& done_callback)
: done_callback_(done_callback) {}
~ForwardingURLFetcherDelegate() override {}
// URLFetcherDelegate API method. Defined below since we need the
// PpdProviderImpl definition first.
void OnURLFetchComplete(const net::URLFetcher* source) override {
done_callback_.Run();
}
private:
// Callback to be run on fetch complete.
base::Callback<void()> done_callback_;
};
class PpdProviderImpl : public PpdProvider {
public:
PpdProviderImpl(
const std::string& api_key,
scoped_refptr<net::URLRequestContextGetter> url_context_getter,
scoped_refptr<base::SequencedTaskRunner> io_task_runner,
std::unique_ptr<PpdCache> cache,
const PpdProvider::Options& options)
: api_key_(api_key),
url_context_getter_(url_context_getter),
io_task_runner_(io_task_runner),
cache_(std::move(cache)),
options_(options),
weak_factory_(this) {
CHECK_GT(options_.max_ppd_contents_size_, 0U);
}
~PpdProviderImpl() override {}
void Resolve(const Printer::PpdReference& ppd_reference,
const PpdProvider::ResolveCallback& cb) override {
CHECK(!cb.is_null());
CHECK(resolve_sequence_checker_.CalledOnValidSequence());
CHECK(base::SequencedTaskRunnerHandle::IsSet())
<< "Resolve must be called from a SequencedTaskRunner context";
CHECK(!resolve_inflight_)
<< "Can't have concurrent PpdProvider Resolve calls";
resolve_inflight_ = true;
auto cache_result = base::MakeUnique<base::Optional<base::FilePath>>();
bool post_result = io_task_runner_->PostTaskAndReply(
FROM_HERE, base::Bind(&PpdProviderImpl::ResolveDoCacheLookup,
weak_factory_.GetWeakPtr(), ppd_reference,
cache_result.get()),
base::Bind(&PpdProviderImpl::ResolveCacheLookupDone,
weak_factory_.GetWeakPtr(), ppd_reference, cb,
std::move(cache_result)));
DCHECK(post_result);
}
void QueryAvailable(const QueryAvailableCallback& cb) override {
CHECK(!cb.is_null());
CHECK(base::SequencedTaskRunnerHandle::IsSet())
<< "QueryAvailable() must be called from a SequencedTaskRunner context";
auto cache_result = base::MakeUnique<
base::Optional<const PpdProvider::AvailablePrintersMap*>>();
CHECK(!query_inflight_)
<< "Can't have concurrent PpdProvider QueryAvailable calls";
query_inflight_ = true;
CHECK(io_task_runner_->PostTaskAndReply(
FROM_HERE, base::Bind(&PpdProviderImpl::QueryAvailableDoCacheLookup,
weak_factory_.GetWeakPtr(), cache_result.get()),
base::Bind(&PpdProviderImpl::QueryAvailableCacheLookupDone,
weak_factory_.GetWeakPtr(), cb, std::move(cache_result))));
}
bool CachePpd(const Printer::PpdReference& ppd_reference,
const base::FilePath& ppd_path) override {
std::string buf;
if (!base::ReadFileToStringWithMaxSize(ppd_path, &buf,
options_.max_ppd_contents_size_)) {
return false;
}
return static_cast<bool>(cache_->Store(ppd_reference, buf));
}
private:
// Trivial wrappers around PpdCache::Find() and
// PpdCache::FindAvailablePrinters(). We need these wrappers both because we
// use weak_ptrs to manage lifetime, and so we need to bind callbacks to
// *this*, and because weak_ptr's preclude return values in posted tasks, so
// we have to use a parameter to return state.
void ResolveDoCacheLookup(
const Printer::PpdReference& reference,
base::Optional<base::FilePath>* cache_result) const {
*cache_result = cache_->Find(reference);
}
void QueryAvailableDoCacheLookup(
base::Optional<const PpdProvider::AvailablePrintersMap*>* cache_result)
const {
auto tmp = cache_->FindAvailablePrinters();
if (tmp != nullptr) {
*cache_result = tmp;
} else {
*cache_result = base::nullopt;
}
}
// Callback that happens when the Resolve() cache lookup completes. If the
// cache satisfied the request, finish the Resolve, otherwise start a URL
// request to satisfy the request. This runs on the same thread as Resolve()
// was invoked on.
void ResolveCacheLookupDone(
const Printer::PpdReference& ppd_reference,
const PpdProvider::ResolveCallback& done_callback,
const std::unique_ptr<base::Optional<base::FilePath>>& cache_result) {
CHECK(resolve_sequence_checker_.CalledOnValidSequence());
if (*cache_result) {
// Cache hit. Schedule the callback now and return.
resolve_inflight_ = false;
done_callback.Run(PpdProvider::SUCCESS, cache_result->value());
return;
}
// We don't have a way to automatically resolve user-supplied PPDs yet. So
// if we have one specified, and it's not cached, we fail out rather than
// fall back to quirks-server based resolution. The reasoning here is that
// if the user has specified a PPD when a quirks-server one exists, it
// probably means the quirks server one doesn't work for some reason, so we
// shouldn't silently use it.
if (!ppd_reference.user_supplied_ppd_url.empty()) {
resolve_inflight_ = false;
done_callback.Run(PpdProvider::NOT_FOUND, base::FilePath());
return;
}
// Missed in the cache, so start a URLRequest to resolve the request.
resolve_fetcher_delegate_ = base::MakeUnique<ForwardingURLFetcherDelegate>(
base::Bind(&PpdProviderImpl::OnResolveFetchComplete,
weak_factory_.GetWeakPtr(), ppd_reference, done_callback));
resolve_fetcher_ = net::URLFetcher::Create(
GetQuirksServerPpdLookupURL(ppd_reference), net::URLFetcher::GET,
resolve_fetcher_delegate_.get());
resolve_fetcher_->SetRequestContext(url_context_getter_.get());
resolve_fetcher_->SetLoadFlags(
net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE |
net::LOAD_DO_NOT_SAVE_COOKIES | net::LOAD_DO_NOT_SEND_COOKIES |
net::LOAD_DO_NOT_SEND_AUTH_DATA);
resolve_fetcher_->Start();
}
// Called on the same thread as Resolve() when the fetcher completes its
// fetch.
void OnResolveFetchComplete(
const Printer::PpdReference& ppd_reference,
const PpdProvider::ResolveCallback& done_callback) {
CHECK(resolve_sequence_checker_.CalledOnValidSequence());
// Scope the allocated |resolve_fetcher_| and |resolve_fetcher_delegate_|
// into this function so we clean it up when we're done here instead of
// leaving it around until the next Resolve() call.
auto fetcher_delegate(std::move(resolve_fetcher_delegate_));
auto fetcher(std::move(resolve_fetcher_));
resolve_inflight_ = false;
std::string contents;
if (!ValidateAndGetResponseAsString(*fetcher, &contents)) {
// Something went wrong with the fetch.
done_callback.Run(PpdProvider::SERVER_ERROR, base::FilePath());
return;
}
auto dict = base::DictionaryValue::From(base::JSONReader::Read(contents));
if (dict == nullptr) {
done_callback.Run(PpdProvider::SERVER_ERROR, base::FilePath());
return;
}
std::string ppd_contents;
std::string last_updated_time_string;
uint64_t last_updated_time;
if (!(dict->GetString(kJSONPPDKey, &ppd_contents) &&
dict->GetString(kJSONLastUpdatedKey, &last_updated_time_string) &&
base::StringToUint64(last_updated_time_string, &last_updated_time))) {
// Malformed response. TODO(justincarlson) - LOG something here?
done_callback.Run(PpdProvider::SERVER_ERROR, base::FilePath());
return;
}
if (ppd_contents.size() > options_.max_ppd_contents_size_) {
// PPD is too big.
//
// Note -- if we ever add shared-ppd-sourcing, e.g. we may serve a ppd to
// a user that's not from an explicitly trusted source, we should also
// check *uncompressed* size here to head off zip-bombs (e.g. let's
// compress 1GBs of zeros into a 900kb file and see what cups does when it
// tries to expand that...)
done_callback.Run(PpdProvider::SERVER_ERROR, base::FilePath());
return;
}
auto ppd_file = cache_->Store(ppd_reference, ppd_contents);
if (!ppd_file) {
// Failed to store.
done_callback.Run(PpdProvider::INTERNAL_ERROR, base::FilePath());
return;
}
done_callback.Run(PpdProvider::SUCCESS, ppd_file.value());
}
// Called on the same thread as QueryAvailable() when the cache lookup is
// done. If the cache satisfied the request, finish the Query, otherwise
// start a URL request to satisfy the Query. This runs on the same thread as
// QueryAvailable() was invoked on.
void QueryAvailableCacheLookupDone(
const PpdProvider::QueryAvailableCallback& done_callback,
const std::unique_ptr<
base::Optional<const PpdProvider::AvailablePrintersMap*>>&
cache_result) {
CHECK(query_sequence_checker_.CalledOnValidSequence());
if (*cache_result) {
query_inflight_ = false;
done_callback.Run(PpdProvider::SUCCESS, *cache_result->value());
return;
}
// Missed in the cache, start a query.
query_fetcher_delegate_ = base::MakeUnique<ForwardingURLFetcherDelegate>(
base::Bind(&PpdProviderImpl::OnQueryAvailableFetchComplete,
weak_factory_.GetWeakPtr(), done_callback));
query_fetcher_ = net::URLFetcher::Create(GetQuirksServerPpdListURL(),
net::URLFetcher::GET,
query_fetcher_delegate_.get());
query_fetcher_->SetRequestContext(url_context_getter_.get());
query_fetcher_->SetLoadFlags(
net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE |
net::LOAD_DO_NOT_SAVE_COOKIES | net::LOAD_DO_NOT_SEND_COOKIES |
net::LOAD_DO_NOT_SEND_AUTH_DATA);
query_fetcher_->Start();
}
void OnQueryAvailableFetchComplete(
const PpdProvider::QueryAvailableCallback& done_callback) {
CHECK(query_sequence_checker_.CalledOnValidSequence());
// Scope the object fetcher and task_runner into this function so we clean
// it up when we're done here instead of leaving it around until the next
// QueryAvailable() call.
auto fetcher_delegate(std::move(query_fetcher_delegate_));
auto fetcher(std::move(query_fetcher_));
query_inflight_ = false;
std::string contents;
if (!ValidateAndGetResponseAsString(*fetcher, &contents)) {
// Something went wrong with the fetch.
done_callback.Run(PpdProvider::SERVER_ERROR, AvailablePrintersMap());
return;
}
// The server gives us JSON in the form of a list of (manufacturer, list of
// models) tuples.
auto top_dict =
base::DictionaryValue::From(base::JSONReader::Read(contents));
const base::ListValue* top_list;
if (top_dict == nullptr || !top_dict->GetList(kJSONTopListKey, &top_list)) {
LOG(ERROR) << "Malformed response from quirks server";
done_callback.Run(PpdProvider::SERVER_ERROR, AvailablePrintersMap());
return;
}
auto result = base::MakeUnique<PpdProvider::AvailablePrintersMap>();
for (const std::unique_ptr<base::Value>& entry : *top_list) {
base::DictionaryValue* dict;
std::string manufacturer;
std::string model;
base::ListValue* model_list;
if (!entry->GetAsDictionary(&dict) ||
!dict->GetString(kJSONManufacturer, &manufacturer) ||
!dict->GetList(kJSONModelList, &model_list)) {
LOG(ERROR) << "Unexpected contents in quirks server printer list.";
// Just skip this entry instead of aborting the whole thing.
continue;
}
std::vector<std::string>& dest = (*result)[manufacturer];
for (const std::unique_ptr<base::Value>& model_value : *model_list) {
if (model_value->GetAsString(&model)) {
dest.push_back(model);
} else {
LOG(ERROR) << "Skipping unknown model for manufacturer "
<< manufacturer;
}
}
}
done_callback.Run(PpdProvider::SUCCESS, *result);
if (!result->empty()) {
cache_->StoreAvailablePrinters(std::move(result));
} else {
// An empty map means something is probably wrong; if we cache this map,
// we'll have an empty map until the cache expires. So complain and
// refuse to cache.
LOG(ERROR) << "Available printers map is unexpectedly empty. Refusing "
"to cache this.";
}
}
// Generate a url to look up a manufacturer/model from the quirks server
GURL GetQuirksServerPpdLookupURL(
const Printer::PpdReference& ppd_reference) const {
return GURL(base::StringPrintf(
"https://%s/v2/printer/manufacturers/%s/models/%s?key=%s",
options_.quirks_server.c_str(),
ppd_reference.effective_manufacturer.c_str(),
ppd_reference.effective_model.c_str(), api_key_.c_str()));
}
// Generate a url to ask for the full supported printer list from the quirks
// server.
GURL GetQuirksServerPpdListURL() const {
return GURL(base::StringPrintf("https://%s/v2/printer/list?key=%s",
options_.quirks_server.c_str(),
api_key_.c_str()));
}
// If |fetcher| succeeded in its fetch, get the response in |response| and
// return true, otherwise return false.
bool ValidateAndGetResponseAsString(const net::URLFetcher& fetcher,
std::string* contents) {
return ((fetcher.GetStatus().status() == net::URLRequestStatus::SUCCESS) &&
(fetcher.GetResponseCode() == net::HTTP_OK) &&
fetcher.GetResponseAsString(contents));
}
// State held across a Resolve() call.
// Check that Resolve() and its callback are sequenced appropriately.
base::SequenceChecker resolve_sequence_checker_;
// Fetcher and associated delegate for the current Resolve() call, if a fetch
// is in progress. These are both null if no Resolve() is in flight.
std::unique_ptr<net::URLFetcher> resolve_fetcher_;
std::unique_ptr<ForwardingURLFetcherDelegate> resolve_fetcher_delegate_;
// Is there a current Resolve() inflight? Used to help fail-fast in the case
// of inappropriate concurrent usage.
bool resolve_inflight_ = false;
// State held across a QueryAvailable() call.
// Check that QueryAvailable() and its callback are sequenced appropriately.
base::SequenceChecker query_sequence_checker_;
// Fetcher and associated delegate for the current QueryAvailable() call, if a
// fetch is in progress. These are both null if no QueryAvailable() is in
// flight.
std::unique_ptr<net::URLFetcher> query_fetcher_;
std::unique_ptr<ForwardingURLFetcherDelegate> query_fetcher_delegate_;
// Is there a current QueryAvailable() inflight? Used to help fail-fast in
// the case of inappropriate concurrent usage.
bool query_inflight_ = false;
// Common state.
// API key for accessing quirks server.
const std::string api_key_;
scoped_refptr<net::URLRequestContextGetter> url_context_getter_;
scoped_refptr<base::SequencedTaskRunner> io_task_runner_;
std::unique_ptr<PpdCache> cache_;
// Construction-time options, immutable.
const PpdProvider::Options options_;
base::WeakPtrFactory<PpdProviderImpl> weak_factory_;
};
} // namespace
// static
std::unique_ptr<PpdProvider> PpdProvider::Create(
const std::string& api_key,
scoped_refptr<net::URLRequestContextGetter> url_context_getter,
scoped_refptr<base::SequencedTaskRunner> io_task_runner,
std::unique_ptr<PpdCache> cache,
const PpdProvider::Options& options) {
return base::MakeUnique<PpdProviderImpl>(
api_key, url_context_getter, io_task_runner, std::move(cache), options);
}
} // namespace printing
} // namespace chromeos