Chromium Code Reviews| OLD | NEW | 
|---|---|
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be | 
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. | 
| 4 | 4 | 
| 5 #include "chromeos/printing/ppd_cache.h" | 5 #include "chromeos/printing/ppd_cache.h" | 
| 6 | 6 | 
| 7 #include <utility> | 7 #include <utility> | 
| 8 #include <vector> | 8 #include <vector> | 
| 9 | 9 | 
| 10 #include "base/bind.h" | |
| 10 #include "base/files/file_util.h" | 11 #include "base/files/file_util.h" | 
| 11 #include "base/json/json_parser.h" | 12 #include "base/json/json_parser.h" | 
| 12 #include "base/json/json_writer.h" | 13 #include "base/json/json_writer.h" | 
| 13 #include "base/memory/ptr_util.h" | 14 #include "base/memory/ptr_util.h" | 
| 14 #include "base/strings/string_number_conversions.h" | 15 #include "base/strings/string_number_conversions.h" | 
| 15 #include "base/strings/string_util.h" | 16 #include "base/strings/string_util.h" | 
| 16 #include "base/synchronization/lock.h" | 17 #include "base/synchronization/lock.h" | 
| 18 #include "base/threading/sequenced_task_runner_handle.h" | |
| 17 #include "base/threading/thread_restrictions.h" | 19 #include "base/threading/thread_restrictions.h" | 
| 18 #include "base/time/time.h" | 20 #include "base/time/time.h" | 
| 19 #include "base/values.h" | 21 #include "base/values.h" | 
| 20 #include "crypto/sha2.h" | 22 #include "crypto/sha2.h" | 
| 21 #include "net/base/io_buffer.h" | 23 #include "net/base/io_buffer.h" | 
| 22 #include "net/filter/gzip_header.h" | 24 #include "net/filter/gzip_header.h" | 
| 23 | 25 | 
| 24 namespace chromeos { | 26 namespace chromeos { | 
| 25 namespace printing { | 27 namespace printing { | 
| 26 namespace { | 28 namespace { | 
| 27 | 29 | 
| 28 // Name of the file we use to cache the list of available printer drivers from | 30 // Biggest file we'll store in the cache. | 
| 29 // QuirksServer. This file resides in the cache directory. | 31 constexpr int kMaxCacheItemSize = 100 * 1024; | 
| 30 const char kAvailablePrintersFilename[] = "all_printers.json"; | |
| 31 | |
| 32 // Return true if it looks like contents is already gzipped, false otherwise. | |
| 33 bool IsGZipped(const std::string& contents) { | |
| 34 const char* ignored; | |
| 35 net::GZipHeader header; | |
| 36 return header.ReadMore(contents.data(), contents.size(), &ignored) == | |
| 37 net::GZipHeader::COMPLETE_HEADER; | |
| 38 } | |
| 39 | 32 | 
| 40 class PpdCacheImpl : public PpdCache { | 33 class PpdCacheImpl : public PpdCache { | 
| 41 public: | 34 public: | 
| 42 PpdCacheImpl(const base::FilePath& cache_base_dir, | 35 PpdCacheImpl(const base::FilePath& cache_base_dir, | 
| 43 const PpdCache::Options& options) | 36 const scoped_refptr<base::SequencedTaskRunner>& disk_task_runner) | 
| 44 : cache_base_dir_(cache_base_dir), | 37 : cache_base_dir_(cache_base_dir), disk_task_runner_(disk_task_runner) {} | 
| 45 available_printers_file_( | 38 | 
| 46 cache_base_dir.Append(kAvailablePrintersFilename)), | 39 // Public API functions. | 
| 47 options_(options) {} | 40 void Find(const std::string& key, const FindCallback& cb) override { | 
| 41 // Ensure the cache lives until the op is over. | |
| 42 AddRef(); | |
| 43 ++inflight_ops_; | |
| 44 disk_task_runner_->PostTask( | |
| 45 FROM_HERE, base::Bind(&PpdCacheImpl::FindImpl, this, key, | |
| 46 base::SequencedTaskRunnerHandle::Get(), cb)); | |
| 47 } | |
| 48 | |
| 49 // Store the given contents at the given key. If cb is non-null, it will | |
| 50 // be invoked on completion. | |
| 51 void Store(const std::string& key, | |
| 52 const std::string& contents, | |
| 53 const base::Callback<void()>& cb) override { | |
| 54 AddRef(); | |
| 55 ++inflight_ops_; | |
| 56 disk_task_runner_->PostTask( | |
| 57 FROM_HERE, base::Bind(&PpdCacheImpl::StoreImpl, this, key, contents, | |
| 58 base::SequencedTaskRunnerHandle::Get(), cb)); | |
| 59 } | |
| 60 | |
| 61 bool Idle() const override { return inflight_ops_ == 0; } | |
| 62 | |
| 63 private: | |
| 48 ~PpdCacheImpl() override {} | 64 ~PpdCacheImpl() override {} | 
| 49 | 65 | 
| 50 // Public API functions. | 66 // If the cache doesn't already exist, create it. | 
| 51 base::Optional<base::FilePath> Find( | 67 void MaybeCreateCache() { | 
| 52 const Printer::PpdReference& reference) const override { | 68 if (!base::PathExists(cache_base_dir_)) { | 
| 53 base::AutoLock l(lock_); | 69 base::CreateDirectory(cache_base_dir_); | 
| 54 base::ThreadRestrictions::AssertIOAllowed(); | |
| 55 base::Optional<base::FilePath> ret; | |
| 56 | |
| 57 // We can't know here if we have a gzipped or un-gzipped version, so just | |
| 58 // look for both. | |
| 59 base::FilePath contents_path_base = GetCachePathBase(reference); | |
| 60 for (const std::string& extension : {".ppd", ".ppd.gz"}) { | |
| 61 base::FilePath contents_path = contents_path_base.AddExtension(extension); | |
| 62 if (base::PathExists(contents_path)) { | |
| 63 ret = contents_path; | |
| 64 break; | |
| 65 } | |
| 66 } | 70 } | 
| 67 return ret; | |
| 68 } | 71 } | 
| 69 | 72 | 
| 70 base::Optional<base::FilePath> Store( | 73 // Find implementation, runs on the i/o thread. | 
| 71 const Printer::PpdReference& reference, | 74 void FindImpl(const std::string& key, | 
| 72 const std::string& ppd_contents) override { | 75 const scoped_refptr<base::SequencedTaskRunner>& callback_runner, | 
| 76 const FindCallback& cb) { | |
| 73 base::ThreadRestrictions::AssertIOAllowed(); | 77 base::ThreadRestrictions::AssertIOAllowed(); | 
| 74 base::AutoLock l(lock_); | 78 FindResult result; | 
| 75 if (!EnsureCacheDirectoryExists()) { | 79 result.success = false; | 
| 76 return base::nullopt; | 80 MaybeCreateCache(); | 
| 77 } | 81 base::File file(FilePathForKey(key), | 
| 78 base::Optional<base::FilePath> ret; | 82 base::File::FLAG_OPEN | base::File::FLAG_READ); | 
| 79 base::FilePath contents_path = | |
| 80 GetCachePathBase(reference).AddExtension(".ppd"); | |
| 81 if (IsGZipped(ppd_contents)) { | |
| 82 contents_path = contents_path.AddExtension(".gz"); | |
| 83 } | |
| 84 if (base::WriteFile(contents_path, ppd_contents.data(), | |
| 85 ppd_contents.size()) == | |
| 86 static_cast<int>(ppd_contents.size())) { | |
| 87 ret = contents_path; | |
| 88 } else { | |
| 89 LOG(ERROR) << "Failed to write " << contents_path.LossyDisplayName(); | |
| 90 // Try to clean up the file, as it may have partial contents. Note that | |
| 91 // DeleteFile(nonexistant file) should return true, so failure here means | |
| 92 // something is exceptionally hosed. | |
| 93 if (!base::DeleteFile(contents_path, false)) { | |
| 94 LOG(ERROR) << "Failed to cleanup partially-written file " | |
| 95 << contents_path.LossyDisplayName(); | |
| 96 return ret; | |
| 97 } | |
| 98 } | |
| 99 return ret; | |
| 100 } | |
| 101 | 83 | 
| 102 base::Optional<PpdProvider::AvailablePrintersMap> FindAvailablePrinters() | 84 if (file.IsValid()) { | 
| 103 override { | 85 int64_t len = file.GetLength(); | 
| 104 base::ThreadRestrictions::AssertIOAllowed(); | 86 if (len >= static_cast<int64_t>(crypto::kSHA256Length) && | 
| 105 base::AutoLock l(lock_); | 87 len <= | 
| 106 if (available_printers_ != nullptr && | 88 kMaxCacheItemSize + static_cast<int64_t>(crypto::kSHA256Length)) { | 
| 107 base::Time::Now() - available_printers_timestamp_ < | 89 std::unique_ptr<char[]> buf(new char[len]); | 
| 108 options_.max_available_list_staleness) { | 90 if (file.ReadAtCurrentPos(buf.get(), len) == len) { | 
| 109 // Satisfy from memory cache. | 91 base::StringPiece contents(buf.get(), len - crypto::kSHA256Length); | 
| 110 return *available_printers_; | 92 base::StringPiece checksum(buf.get() + len - crypto::kSHA256Length, | 
| 111 } | 93 crypto::kSHA256Length); | 
| 112 std::string buf; | 94 if (crypto::SHA256HashString(contents) == checksum) { | 
| 
 
skau
2017/01/05 20:38:46
Should we log a warning if this is false?
 
Carlson
2017/01/26 21:59:36
Done.
 
 | |
| 113 if (!MaybeReadAvailablePrintersCache(&buf)) { | 95 base::File::Info info; | 
| 114 // Disk cache miss. | 96 if (file.GetInfo(&info)) { | 
| 115 return base::nullopt; | 97 result.success = true; | 
| 116 } | 98 result.age = base::Time::Now() - info.last_modified; | 
| 117 auto dict = base::DictionaryValue::From(base::JSONReader::Read(buf)); | 99 contents.CopyToString(&result.contents); | 
| 118 if (dict == nullptr) { | 100 } | 
| 119 LOG(ERROR) << "Failed to deserialize available printers cache"; | 101 } | 
| 120 return base::nullopt; | |
| 121 } | |
| 122 // Note if we got here, we've already set available_printers_timestamp_ to | |
| 123 // the mtime of the file we read from. | |
| 124 available_printers_ = base::MakeUnique<PpdProvider::AvailablePrintersMap>(); | |
| 125 const base::ListValue* models; | |
| 126 std::string model; | |
| 127 for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd(); | |
| 128 it.Advance()) { | |
| 129 auto& out = (*available_printers_)[it.key()]; | |
| 130 if (!it.value().GetAsList(&models)) { | |
| 131 LOG(ERROR) << "Skipping malformed printer make: " << it.key(); | |
| 132 continue; | |
| 133 } | |
| 134 for (const auto& model_value : *models) { | |
| 135 if (model_value->GetAsString(&model)) { | |
| 136 out.push_back(model); | |
| 137 } else { | |
| 138 LOG(ERROR) << "Skipping malformed printer model in: " << it.key() | |
| 139 << ". Expected a string, found a " | |
| 140 << base::Value::GetTypeName(model_value->GetType()); | |
| 141 } | 102 } | 
| 142 } | 103 } | 
| 143 } | 104 } | 
| 144 return *available_printers_; | 105 callback_runner->PostTask(FROM_HERE, | 
| 106 base::Bind(&PpdCacheImpl::ReplyAndRelease, this, | |
| 107 base::Bind(cb, result))); | |
| 145 } | 108 } | 
| 146 | 109 | 
| 147 // Note we throw up our hands and fail (gracefully) to store if we encounter | 110 // If |cb| is non-null, invoke it on this thread, then decrement the refcount. | 
| 148 // non-unicode things in the strings of |available_printers|. Since these | 111 void ReplyAndRelease(const base::Callback<void()>& cb) { | 
| 149 // strings come from a source we control, being less paranoid about these | 112 if (!cb.is_null()) { | 
| 150 // values seems reasonable. | 113 cb.Run(); | 
| 151 void StoreAvailablePrinters(std::unique_ptr<PpdProvider::AvailablePrintersMap> | |
| 152 available_printers) override { | |
| 153 base::ThreadRestrictions::AssertIOAllowed(); | |
| 154 base::AutoLock l(lock_); | |
| 155 if (!EnsureCacheDirectoryExists()) { | |
| 156 return; | |
| 157 } | 114 } | 
| 158 available_printers_ = std::move(available_printers); | 115 --inflight_ops_; | 
| 159 available_printers_timestamp_ = base::Time::Now(); | 116 Release(); | 
| 160 // Convert the map to Values, in preparation for jsonification. | 117 // Object may be destroyed here, so no further access to object data | 
| 161 base::DictionaryValue top_level; | 118 // allowed. | 
| 162 for (const auto& entry : *available_printers_) { | |
| 163 auto printers = base::MakeUnique<base::ListValue>(); | |
| 164 printers->AppendStrings(entry.second); | |
| 165 top_level.Set(entry.first, std::move(printers)); | |
| 166 } | |
| 167 std::string contents; | |
| 168 if (!base::JSONWriter::Write(top_level, &contents)) { | |
| 169 LOG(ERROR) << "Failed to generate JSON"; | |
| 170 return; | |
| 171 } | |
| 172 if (contents.size() > options_.max_available_list_cached_size) { | |
| 173 LOG(ERROR) << "Serialized available printers list too large (size is " | |
| 174 << contents.size() << " bytes)"; | |
| 175 return; | |
| 176 } | |
| 177 if (base::WriteFile(available_printers_file_, contents.data(), | |
| 178 contents.size()) != static_cast<int>(contents.size())) { | |
| 179 LOG(ERROR) << "Failed to write available printers cache to " | |
| 180 << available_printers_file_.MaybeAsASCII(); | |
| 181 } | |
| 182 } | 119 } | 
| 183 | 120 | 
| 184 private: | 121 // Store implementation, runs on the i/o thread. | 
| 185 // Create the cache directory if it doesn't already exist. Returns true | 122 void StoreImpl( | 
| 186 // on success. | 123 const std::string& key, | 
| 187 bool EnsureCacheDirectoryExists() { | 124 const std::string& contents, | 
| 188 if (base::PathExists(cache_base_dir_) || | 125 const scoped_refptr<base::SequencedTaskRunner>& callback_runner, | 
| 189 base::CreateDirectory(cache_base_dir_)) { | 126 const base::Callback<void()> cb) { | 
| 190 return true; | 127 base::ThreadRestrictions::AssertIOAllowed(); | 
| 128 MaybeCreateCache(); | |
| 129 if (contents.size() > kMaxCacheItemSize) { | |
| 130 LOG(ERROR) << "Ignoring attempt to cache large object"; | |
| 131 } else { | |
| 132 base::File file(FilePathForKey(key), | |
| 133 base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE); | |
| 134 std::string checksum = crypto::SHA256HashString(contents); | |
| 135 if (!file.IsValid() || | |
| 136 file.WriteAtCurrentPos(contents.data(), contents.size()) != | |
| 137 static_cast<int>(contents.size()) || | |
| 138 file.WriteAtCurrentPos(checksum.data(), checksum.size()) != | |
| 139 static_cast<int>(checksum.size())) { | |
| 140 LOG(ERROR) << "Failed to create ppd cache file"; | |
| 
 
skau
2017/01/05 20:38:46
Do we need to clean up the file if one of the writ
 
Carlson
2017/01/26 21:59:36
Done.
 
 | |
| 141 } | |
| 191 } | 142 } | 
| 192 LOG(ERROR) << "Failed to create ppd cache directory " | 143 callback_runner->PostTask( | 
| 193 << cache_base_dir_.MaybeAsASCII(); | 144 FROM_HERE, base::Bind(&PpdCacheImpl::ReplyAndRelease, this, cb)); | 
| 194 return false; | |
| 195 } | 145 } | 
| 196 | 146 | 
| 197 // Get the file path at which we expect to find a PPD if it's cached. | 147 // Return the (full) path to the file we expect to find the given key at. | 
| 198 // | 148 base::FilePath FilePathForKey(const std::string& key) { | 
| 199 // This is, ultimately, just a hash function. It's extremely infrequently | 149 std::string hashed_key = crypto::SHA256HashString(key); | 
| 200 // used (called once when trying to look up information on a printer or store | 150 return cache_base_dir_.Append( | 
| 201 // a PPD), and should be stable, as changing the function will make previously | 151 base::HexEncode(hashed_key.data(), hashed_key.size())); | 
| 202 // cached entries unfindable, causing resolve logic to be reinvoked | |
| 203 // unnecessarily. | |
| 204 // | |
| 205 // There's also a faint possibility that a bad actor might try to do something | |
| 206 // nefarious by intentionally causing a cache collision that makes the wrong | |
| 207 // PPD be used for a printer. There's no obvious attack vector, but | |
| 208 // there's also no real cost to being paranoid here, so we use SHA-256 as the | |
| 209 // underlying hash function, and inject fixed field prefixes to prevent | |
| 210 // field-substitution spoofing. This also buys us hash function stability at | |
| 211 // the same time. | |
| 212 // | |
| 213 // Also, care should be taken to preserve the existing hash values if new | |
| 214 // fields are added to PpdReference -- that is, if a new field F is added | |
| 215 // to PpdReference, a PpdReference with a default F value should hash to | |
| 216 // the same thing as a PpdReference that predates the addition of F to the | |
| 217 // structure. | |
| 218 // | |
| 219 // Note this function expects that the caller will append ".ppd", or ".ppd.gz" | |
| 220 // to the output as needed. | |
| 221 base::FilePath GetCachePathBase(const Printer::PpdReference& ref) const { | |
| 222 std::vector<std::string> pieces; | |
| 223 if (!ref.user_supplied_ppd_url.empty()) { | |
| 224 pieces.push_back("user_supplied_ppd_url:"); | |
| 225 pieces.push_back(ref.user_supplied_ppd_url); | |
| 226 } else if (!ref.effective_manufacturer.empty() && | |
| 227 !ref.effective_model.empty()) { | |
| 228 pieces.push_back("manufacturer:"); | |
| 229 pieces.push_back(ref.effective_manufacturer); | |
| 230 pieces.push_back("model:"); | |
| 231 pieces.push_back(ref.effective_model); | |
| 232 } else { | |
| 233 NOTREACHED() << "PpdCache hashing empty PpdReference"; | |
| 234 } | |
| 235 // The separator here is not needed, but makes debug output more readable. | |
| 236 std::string full_key = base::JoinString(pieces, "|"); | |
| 237 std::string hashed_key = crypto::SHA256HashString(full_key); | |
| 238 std::string ascii_hash = | |
| 239 base::HexEncode(hashed_key.data(), hashed_key.size()); | |
| 240 VLOG(3) << "PPD Cache key is " << full_key << " which hashes to " | |
| 241 << ascii_hash; | |
| 242 | |
| 243 return cache_base_dir_.Append(ascii_hash); | |
| 244 } | 152 } | 
| 245 | 153 | 
| 246 // Try to read the available printers cache. Returns true on success. On | 154 int inflight_ops_ = 0; | 
| 247 // success, |buf| will contain the contents of the file, otherwise it will be | |
| 248 // cleared. | |
| 249 bool MaybeReadAvailablePrintersCache(std::string* buf) { | |
| 250 buf->clear(); | |
| 251 | 155 | 
| 252 base::File cache_file(available_printers_file_, | 156 base::FilePath cache_base_dir_; | 
| 253 base::File::FLAG_OPEN | base::File::FLAG_READ); | 157 scoped_refptr<base::SequencedTaskRunner> disk_task_runner_; | 
| 254 base::File::Info info; | |
| 255 if (cache_file.IsValid() && cache_file.GetInfo(&info) && | |
| 256 (base::Time::Now() - info.last_modified <= | |
| 257 options_.max_available_list_staleness)) { | |
| 258 // We have a file that's recent enough to use. | |
| 259 if (!base::ReadFileToStringWithMaxSize( | |
| 260 available_printers_file_, buf, | |
| 261 options_.max_available_list_cached_size)) { | |
| 262 LOG(ERROR) << "Failed to read printer cache from " | |
| 263 << available_printers_file_.MaybeAsASCII(); | |
| 264 buf->clear(); | |
| 265 return false; | |
| 266 } | |
| 267 available_printers_timestamp_ = info.last_modified; | |
| 268 return true; | |
| 269 } | |
| 270 // Either we don't have an openable file, or it's too old. | |
| 271 // | |
| 272 // If we have an invalid file and it's not valid for reasons other than | |
| 273 // NOT_FOUND, that's unexpected and worth logging. Otherwise this is | |
| 274 // a normal cache miss. | |
| 275 if (!cache_file.IsValid() && | |
| 276 cache_file.error_details() != base::File::FILE_ERROR_NOT_FOUND) { | |
| 277 LOG(ERROR) << "Unexpected result when attempting to open printer cache: " | |
| 278 << base::File::ErrorToString(cache_file.error_details()); | |
| 279 } | |
| 280 return false; | |
| 281 } | |
| 282 | |
| 283 // In-memory copy of the available printers map, null if we don't have an | |
| 284 // in-memory copy yet. Filled in the first time the map is fetched from | |
| 285 // disk or stored. | |
| 286 std::unique_ptr<PpdProvider::AvailablePrintersMap> available_printers_; | |
| 287 // Timestamp for the in-memory copy of the cache. (The on-disk version uses | |
| 288 // the file mtime). | |
| 289 base::Time available_printers_timestamp_; | |
| 290 | |
| 291 const base::FilePath cache_base_dir_; | |
| 292 const base::FilePath available_printers_file_; | |
| 293 const PpdCache::Options options_; | |
| 294 | |
| 295 mutable base::Lock lock_; | |
| 296 | 158 | 
| 297 DISALLOW_COPY_AND_ASSIGN(PpdCacheImpl); | 159 DISALLOW_COPY_AND_ASSIGN(PpdCacheImpl); | 
| 298 }; | 160 }; | 
| 299 | 161 | 
| 300 } // namespace | 162 } // namespace | 
| 301 | 163 | 
| 302 // static | 164 // static | 
| 303 std::unique_ptr<PpdCache> PpdCache::Create(const base::FilePath& cache_base_dir, | 165 scoped_refptr<PpdCache> PpdCache::Create( | 
| 304 const PpdCache::Options& options) { | 166 const base::FilePath& cache_base_dir, | 
| 305 return base::MakeUnique<PpdCacheImpl>(cache_base_dir, options); | 167 const scoped_refptr<base::SequencedTaskRunner>& disk_task_runner) { | 
| 168 return scoped_refptr<PpdCache>( | |
| 169 new PpdCacheImpl(cache_base_dir, disk_task_runner)); | |
| 306 } | 170 } | 
| 307 | 171 | 
| 308 } // namespace printing | 172 } // namespace printing | 
| 309 } // namespace chromeos | 173 } // namespace chromeos | 
| OLD | NEW |