| // 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_cache.h" |
| |
| #include <utility> |
| #include <vector> |
| |
| #include "base/files/file_util.h" |
| #include "base/json/json_parser.h" |
| #include "base/json/json_writer.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/synchronization/lock.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "base/time/time.h" |
| #include "base/values.h" |
| #include "crypto/sha2.h" |
| #include "net/base/io_buffer.h" |
| #include "net/filter/gzip_header.h" |
| |
| namespace chromeos { |
| namespace printing { |
| namespace { |
| |
| // Name of the file we use to cache the list of available printer drivers from |
| // QuirksServer. This file resides in the cache directory. |
| const char kAvailablePrintersFilename[] = "all_printers.json"; |
| |
| // Return true if it looks like contents is already gzipped, false otherwise. |
| bool IsGZipped(const std::string& contents) { |
| const char* ignored; |
| net::GZipHeader header; |
| return header.ReadMore(contents.data(), contents.size(), &ignored) == |
| net::GZipHeader::COMPLETE_HEADER; |
| } |
| |
| class PpdCacheImpl : public PpdCache { |
| public: |
| PpdCacheImpl(const base::FilePath& cache_base_dir, |
| const PpdCache::Options& options) |
| : cache_base_dir_(cache_base_dir), |
| available_printers_file_( |
| cache_base_dir.Append(kAvailablePrintersFilename)), |
| options_(options) {} |
| ~PpdCacheImpl() override {} |
| |
| // Public API functions. |
| base::Optional<base::FilePath> Find( |
| const Printer::PpdReference& reference) const override { |
| base::AutoLock l(lock_); |
| base::ThreadRestrictions::AssertIOAllowed(); |
| base::Optional<base::FilePath> ret; |
| |
| // We can't know here if we have a gzipped or un-gzipped version, so just |
| // look for both. |
| base::FilePath contents_path_base = GetCachePathBase(reference); |
| for (const std::string& extension : {".ppd", ".ppd.gz"}) { |
| base::FilePath contents_path = contents_path_base.AddExtension(extension); |
| if (base::PathExists(contents_path)) { |
| ret = contents_path; |
| break; |
| } |
| } |
| return ret; |
| } |
| |
| base::Optional<base::FilePath> Store( |
| const Printer::PpdReference& reference, |
| const std::string& ppd_contents) override { |
| base::ThreadRestrictions::AssertIOAllowed(); |
| base::AutoLock l(lock_); |
| if (!EnsureCacheDirectoryExists()) { |
| return base::nullopt; |
| } |
| base::Optional<base::FilePath> ret; |
| base::FilePath contents_path = |
| GetCachePathBase(reference).AddExtension(".ppd"); |
| if (IsGZipped(ppd_contents)) { |
| contents_path = contents_path.AddExtension(".gz"); |
| } |
| if (base::WriteFile(contents_path, ppd_contents.data(), |
| ppd_contents.size()) == |
| static_cast<int>(ppd_contents.size())) { |
| ret = contents_path; |
| } else { |
| LOG(ERROR) << "Failed to write " << contents_path.LossyDisplayName(); |
| // Try to clean up the file, as it may have partial contents. Note that |
| // DeleteFile(nonexistant file) should return true, so failure here means |
| // something is exceptionally hosed. |
| if (!base::DeleteFile(contents_path, false)) { |
| LOG(ERROR) << "Failed to cleanup partially-written file " |
| << contents_path.LossyDisplayName(); |
| return ret; |
| } |
| } |
| return ret; |
| } |
| |
| base::Optional<PpdProvider::AvailablePrintersMap> FindAvailablePrinters() |
| override { |
| base::ThreadRestrictions::AssertIOAllowed(); |
| base::AutoLock l(lock_); |
| if (available_printers_ != nullptr && |
| base::Time::Now() - available_printers_timestamp_ < |
| options_.max_available_list_staleness) { |
| // Satisfy from memory cache. |
| return *available_printers_; |
| } |
| std::string buf; |
| if (!MaybeReadAvailablePrintersCache(&buf)) { |
| // Disk cache miss. |
| return base::nullopt; |
| } |
| auto dict = base::DictionaryValue::From(base::JSONReader::Read(buf)); |
| if (dict == nullptr) { |
| LOG(ERROR) << "Failed to deserialize available printers cache"; |
| return base::nullopt; |
| } |
| // Note if we got here, we've already set available_printers_timestamp_ to |
| // the mtime of the file we read from. |
| available_printers_ = base::MakeUnique<PpdProvider::AvailablePrintersMap>(); |
| const base::ListValue* models; |
| std::string model; |
| for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd(); |
| it.Advance()) { |
| auto& out = (*available_printers_)[it.key()]; |
| if (!it.value().GetAsList(&models)) { |
| LOG(ERROR) << "Skipping malformed printer make: " << it.key(); |
| continue; |
| } |
| for (const auto& model_value : *models) { |
| if (model_value->GetAsString(&model)) { |
| out.push_back(model); |
| } else { |
| LOG(ERROR) << "Skipping malformed printer model in: " << it.key() |
| << ". Expected a string, found a " |
| << base::Value::GetTypeName(model_value->GetType()); |
| } |
| } |
| } |
| return *available_printers_; |
| } |
| |
| // Note we throw up our hands and fail (gracefully) to store if we encounter |
| // non-unicode things in the strings of |available_printers|. Since these |
| // strings come from a source we control, being less paranoid about these |
| // values seems reasonable. |
| void StoreAvailablePrinters(std::unique_ptr<PpdProvider::AvailablePrintersMap> |
| available_printers) override { |
| base::ThreadRestrictions::AssertIOAllowed(); |
| base::AutoLock l(lock_); |
| if (!EnsureCacheDirectoryExists()) { |
| return; |
| } |
| available_printers_ = std::move(available_printers); |
| available_printers_timestamp_ = base::Time::Now(); |
| // Convert the map to Values, in preparation for jsonification. |
| base::DictionaryValue top_level; |
| for (const auto& entry : *available_printers_) { |
| auto printers = base::MakeUnique<base::ListValue>(); |
| printers->AppendStrings(entry.second); |
| top_level.Set(entry.first, std::move(printers)); |
| } |
| std::string contents; |
| if (!base::JSONWriter::Write(top_level, &contents)) { |
| LOG(ERROR) << "Failed to generate JSON"; |
| return; |
| } |
| if (contents.size() > options_.max_available_list_cached_size) { |
| LOG(ERROR) << "Serialized available printers list too large (size is " |
| << contents.size() << " bytes)"; |
| return; |
| } |
| if (base::WriteFile(available_printers_file_, contents.data(), |
| contents.size()) != static_cast<int>(contents.size())) { |
| LOG(ERROR) << "Failed to write available printers cache to " |
| << available_printers_file_.MaybeAsASCII(); |
| } |
| } |
| |
| private: |
| // Create the cache directory if it doesn't already exist. Returns true |
| // on success. |
| bool EnsureCacheDirectoryExists() { |
| if (base::PathExists(cache_base_dir_) || |
| base::CreateDirectory(cache_base_dir_)) { |
| return true; |
| } |
| LOG(ERROR) << "Failed to create ppd cache directory " |
| << cache_base_dir_.MaybeAsASCII(); |
| return false; |
| } |
| |
| // Get the file path at which we expect to find a PPD if it's cached. |
| // |
| // This is, ultimately, just a hash function. It's extremely infrequently |
| // used (called once when trying to look up information on a printer or store |
| // a PPD), and should be stable, as changing the function will make previously |
| // cached entries unfindable, causing resolve logic to be reinvoked |
| // unnecessarily. |
| // |
| // There's also a faint possibility that a bad actor might try to do something |
| // nefarious by intentionally causing a cache collision that makes the wrong |
| // PPD be used for a printer. There's no obvious attack vector, but |
| // there's also no real cost to being paranoid here, so we use SHA-256 as the |
| // underlying hash function, and inject fixed field prefixes to prevent |
| // field-substitution spoofing. This also buys us hash function stability at |
| // the same time. |
| // |
| // Also, care should be taken to preserve the existing hash values if new |
| // fields are added to PpdReference -- that is, if a new field F is added |
| // to PpdReference, a PpdReference with a default F value should hash to |
| // the same thing as a PpdReference that predates the addition of F to the |
| // structure. |
| // |
| // Note this function expects that the caller will append ".ppd", or ".ppd.gz" |
| // to the output as needed. |
| base::FilePath GetCachePathBase(const Printer::PpdReference& ref) const { |
| std::vector<std::string> pieces; |
| if (!ref.user_supplied_ppd_url.empty()) { |
| pieces.push_back("user_supplied_ppd_url:"); |
| pieces.push_back(ref.user_supplied_ppd_url); |
| } else if (!ref.effective_manufacturer.empty() && |
| !ref.effective_model.empty()) { |
| pieces.push_back("manufacturer:"); |
| pieces.push_back(ref.effective_manufacturer); |
| pieces.push_back("model:"); |
| pieces.push_back(ref.effective_model); |
| } else { |
| NOTREACHED() << "PpdCache hashing empty PpdReference"; |
| } |
| // The separator here is not needed, but makes debug output more readable. |
| std::string full_key = base::JoinString(pieces, "|"); |
| std::string hashed_key = crypto::SHA256HashString(full_key); |
| std::string ascii_hash = |
| base::HexEncode(hashed_key.data(), hashed_key.size()); |
| VLOG(3) << "PPD Cache key is " << full_key << " which hashes to " |
| << ascii_hash; |
| |
| return cache_base_dir_.Append(ascii_hash); |
| } |
| |
| // Try to read the available printers cache. Returns true on success. On |
| // success, |buf| will contain the contents of the file, otherwise it will be |
| // cleared. |
| bool MaybeReadAvailablePrintersCache(std::string* buf) { |
| buf->clear(); |
| |
| base::File cache_file(available_printers_file_, |
| base::File::FLAG_OPEN | base::File::FLAG_READ); |
| base::File::Info info; |
| if (cache_file.IsValid() && cache_file.GetInfo(&info) && |
| (base::Time::Now() - info.last_modified <= |
| options_.max_available_list_staleness)) { |
| // We have a file that's recent enough to use. |
| if (!base::ReadFileToStringWithMaxSize( |
| available_printers_file_, buf, |
| options_.max_available_list_cached_size)) { |
| LOG(ERROR) << "Failed to read printer cache from " |
| << available_printers_file_.MaybeAsASCII(); |
| buf->clear(); |
| return false; |
| } |
| available_printers_timestamp_ = info.last_modified; |
| return true; |
| } |
| // Either we don't have an openable file, or it's too old. |
| // |
| // If we have an invalid file and it's not valid for reasons other than |
| // NOT_FOUND, that's unexpected and worth logging. Otherwise this is |
| // a normal cache miss. |
| if (!cache_file.IsValid() && |
| cache_file.error_details() != base::File::FILE_ERROR_NOT_FOUND) { |
| LOG(ERROR) << "Unexpected result when attempting to open printer cache: " |
| << base::File::ErrorToString(cache_file.error_details()); |
| } |
| return false; |
| } |
| |
| // In-memory copy of the available printers map, null if we don't have an |
| // in-memory copy yet. Filled in the first time the map is fetched from |
| // disk or stored. |
| std::unique_ptr<PpdProvider::AvailablePrintersMap> available_printers_; |
| // Timestamp for the in-memory copy of the cache. (The on-disk version uses |
| // the file mtime). |
| base::Time available_printers_timestamp_; |
| |
| const base::FilePath cache_base_dir_; |
| const base::FilePath available_printers_file_; |
| const PpdCache::Options options_; |
| |
| mutable base::Lock lock_; |
| |
| DISALLOW_COPY_AND_ASSIGN(PpdCacheImpl); |
| }; |
| |
| } // namespace |
| |
| // static |
| std::unique_ptr<PpdCache> PpdCache::Create(const base::FilePath& cache_base_dir, |
| const PpdCache::Options& options) { |
| return base::MakeUnique<PpdCacheImpl>(cache_base_dir, options); |
| } |
| |
| } // namespace printing |
| } // namespace chromeos |