Chromium Code Reviews| Index: chrome/browser/media/router/discovery/dial/device_description_service.cc |
| diff --git a/chrome/browser/media/router/discovery/dial/device_description_service.cc b/chrome/browser/media/router/discovery/dial/device_description_service.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..f121d918cc2dfe5b9052195f3012c5bfdf39be4e |
| --- /dev/null |
| +++ b/chrome/browser/media/router/discovery/dial/device_description_service.cc |
| @@ -0,0 +1,314 @@ |
| +// Copyright 2017 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 "chrome/browser/media/router/discovery/dial/device_description_service.h" |
| + |
| +#ifndef NDEBUG |
| +#include <sstream> |
| +#endif |
| + |
| +#include "base/stl_util.h" |
| +#include "base/strings/string_util.h" |
| +#include "chrome/browser/media/router/discovery/dial/device_description_fetcher.h" |
| +#include "chrome/browser/media/router/discovery/dial/safe_dial_device_description_parser.h" |
| +#include "net/base/ip_address.h" |
| +#include "url/gurl.h" |
| + |
| +namespace { |
| + |
| +enum ErrorType { |
| + NONE, |
| + MISSING_UNIQUE_ID, |
| + MISSING_FRIENDLY_NAME, |
| + MISSING_APP_URL, |
| + INVALID_APP_URL |
| +}; |
| + |
| +// How long to cache a device description. |
| +const int kDeviceDescriptionCacheTimeMins = 12 * 60; |
|
imcheng
2017/04/25 01:40:12
Just 12 here?
imcheng
2017/04/25 01:40:12
constexpr here and below.
mark a. foltz
2017/04/25 21:06:25
I assume you also meant s/Mins/Hours/?
zhaobin
2017/04/26 18:50:04
Acknowledged.
zhaobin
2017/04/26 18:50:04
Done.
zhaobin
2017/04/26 18:50:04
Done.
|
| + |
| +// Time interval to clean up cache entries. |
| +const int kCacheCleanUpTimeoutMins = 30; |
| + |
| +// Maximum size on the number of cached entries. |
| +const int kCacheMaxEntries = 256; |
| + |
| +#ifndef NDEBUG |
| +// Replaces "<element_name>content</element_name>" with |
| +// "<element_name>***</element_name>" |
| +void Scrub(const std::string& element_name, std::string* xml_text) { |
| + size_t pos = xml_text->find("<" + element_name + ">"); |
| + size_t end_pos = xml_text->find("</" + element_name + ">"); |
| + |
| + if (pos == std::string::npos || end_pos == std::string::npos) |
| + return; |
| + |
| + size_t start_pos = pos + element_name.length() + 2; |
| + if (end_pos > start_pos) |
| + xml_text->replace(start_pos, end_pos - start_pos, "***"); |
| +} |
| + |
| +// Removes unique identifiers from the device description. |
| +// |xml_text|: The device description XML. |
| +// Returns original XML text if <UDN> or <serialNumber> field does not exist. |
| +std::string ScrubDeviceDescriptionXml(const std::string& xml_text) { |
| + std::string scrubbed_xml(xml_text); |
| + Scrub("UDN", &scrubbed_xml); |
| + Scrub("serialNumber", &scrubbed_xml); |
| + return scrubbed_xml; |
| +} |
| + |
| +std::string CachedDeviceDescriptionToString( |
| + const media_router::DeviceDescriptionService::CacheEntry& cached_data) { |
| + std::stringstream ss; |
| + ss << "CachedDialDeviceDescription [unique_id]: " |
| + << cached_data.description_data.unique_id |
| + << " [friendly_name]: " << cached_data.description_data.friendly_name |
| + << " [model_name]: " << cached_data.description_data.model_name |
| + << " [app_url]: " << cached_data.description_data.app_url |
| + << " [expire_time]: " << cached_data.expire_time |
| + << " [config_id]: " << cached_data.config_id; |
| + |
| + return ss.str(); |
| +} |
| +#endif |
| + |
| +bool IsValidAppUrl(const GURL& app_url, const std::string& ip_address) { |
| + return app_url.SchemeIs(url::kHttpScheme) && app_url.host() == ip_address; |
| +} |
| + |
| +// Checks mandatory fields. Returns ErrorType::NONE if device description is |
| +// valid; Otherwise returns specific error type. |
| +ErrorType ValidateParsedDeviceDescription( |
| + const std::string& expected_ip_address, |
| + const media_router::ParsedDialDeviceDescription& description_data) { |
| + if (description_data.unique_id.empty()) { |
| + return ErrorType::MISSING_UNIQUE_ID; |
| + } |
| + if (description_data.friendly_name.empty()) { |
| + return ErrorType::MISSING_FRIENDLY_NAME; |
| + } |
| + if (!description_data.app_url.is_valid()) { |
| + return ErrorType::MISSING_APP_URL; |
| + } |
| + if (!IsValidAppUrl(description_data.app_url, expected_ip_address)) { |
| + return ErrorType::INVALID_APP_URL; |
| + } |
| + return ErrorType::NONE; |
| +} |
| + |
| +} // namespace |
| + |
| +namespace media_router { |
| + |
| +DeviceDescriptionService::DeviceDescriptionService( |
| + const DeviceDescriptionParseSuccessCallback& success_cb, |
| + const DeviceDescriptionParseErrorCallback& error_cb) |
| + : success_cb_(success_cb), error_cb_(error_cb) {} |
| + |
| +DeviceDescriptionService::~DeviceDescriptionService() { |
| + if (!pending_device_labels_.empty()) { |
| + DLOG(WARNING) << "Fail to finish parsing " << pending_device_labels_.size() |
| + << " devices."; |
| + } |
| +} |
| + |
| +void DeviceDescriptionService::GetDeviceDescriptions( |
| + const std::vector<DialDeviceData>& devices, |
| + net::URLRequestContextGetter* request_context) { |
| + DCHECK(thread_checker_.CalledOnValidThread()); |
| + |
| + std::map<std::string, std::unique_ptr<DeviceDescriptionFetcher>> |
| + existing_fetcher_map; |
| + for (auto& fetcher_it : device_description_fetcher_map_) { |
| + std::string device_label = fetcher_it.first; |
| + const auto& device_it = |
| + std::find_if(devices.begin(), devices.end(), |
| + [&device_label](const DialDeviceData& device_data) { |
| + return device_data.label() == device_label; |
| + }); |
| + if (device_it == devices.end() || |
| + device_it->device_description_url() == |
| + fetcher_it.second->device_description_url()) { |
| + existing_fetcher_map.insert( |
| + std::make_pair(device_label, std::move(fetcher_it.second))); |
| + } |
| + } |
| + |
| + // Remove all out dated fetchers. |
| + device_description_fetcher_map_ = std::move(existing_fetcher_map); |
|
imcheng
2017/04/25 01:40:12
If we destroyed any old fetchers here, wouldn't be
mark a. foltz
2017/04/25 21:06:25
It looks like you could wait to insert the label i
zhaobin
2017/04/26 18:50:04
Need to insert it here, so utility process is kept
zhaobin
2017/04/26 18:50:04
Discussed with Derek offline. We do not need to re
|
| + |
| + for (const auto& device_data : devices) { |
| + auto* cache_entry = CheckAndUpdateCache(device_data); |
| + if (cache_entry) { |
| + // Get device description from cache. |
| + success_cb_.Run(device_data, cache_entry->description_data); |
| + continue; |
| + } |
| + |
| + FetchDeviceDescription(device_data, request_context); |
| + } |
| + |
| + // Start a clean up timer. |
| + if (!clean_up_timer_) { |
| + clean_up_timer_.reset(new base::RepeatingTimer()); |
| + clean_up_timer_->Start( |
| + FROM_HERE, base::TimeDelta::FromMinutes(kCacheCleanUpTimeoutMins), this, |
| + &DeviceDescriptionService::CleanUpCacheEntries); |
| + } |
| +} |
| + |
| +void DeviceDescriptionService::CleanUpCacheEntries() { |
| + DCHECK(thread_checker_.CalledOnValidThread()); |
| + base::Time now = GetNow(); |
| + |
| + DVLOG(2) << "Before clean up, cache size: " << description_map_.size(); |
| + base::EraseIf(description_map_, |
| + [&now](const std::pair<std::string, CacheEntry>& cache_pair) { |
| + return cache_pair.second.expire_time < now; |
|
imcheng
2017/04/25 01:40:12
It looks like the predicate is reversed?
zhaobin
2017/04/26 18:50:04
Seems ok? If exprie time < now(), the entry is exp
imcheng
2017/04/26 22:52:15
Ah of course. You are right.
zhaobin
2017/04/27 01:41:20
Acknowledged.
|
| + }); |
| + DVLOG(2) << "After clean up, cache size: " << description_map_.size(); |
| + |
| + if (description_map_.empty() && device_description_fetcher_map_.empty()) { |
| + DVLOG(2) << "Cache is empty, stop clean up timer..."; |
| + clean_up_timer_.reset(); |
| + } |
| +} |
| + |
| +void DeviceDescriptionService::FetchDeviceDescription( |
| + const DialDeviceData& device_data, |
| + net::URLRequestContextGetter* request_context) { |
| + DCHECK(thread_checker_.CalledOnValidThread()); |
| + |
| + auto device_description_fetcher = |
| + base::WrapUnique(new DeviceDescriptionFetcher( |
| + device_data.device_description_url(), request_context, |
| + base::BindOnce( |
| + &DeviceDescriptionService::OnDeviceDescriptionFetchComplete, |
| + base::Unretained(this), device_data), |
| + base::BindOnce( |
| + &DeviceDescriptionService::OnDeviceDescriptionFetchError, |
| + base::Unretained(this), device_data))); |
| + |
| + device_description_fetcher->Start(); |
| + device_description_fetcher_map_.insert(std::make_pair( |
| + device_data.label(), std::move(device_description_fetcher))); |
| + |
| + pending_device_labels_.insert(device_data.label()); |
| +} |
| + |
| +const DeviceDescriptionService::CacheEntry* |
| +DeviceDescriptionService::CheckAndUpdateCache( |
| + const DialDeviceData& device_data) { |
| + const auto& it = description_map_.find(device_data.label()); |
| + if (it == description_map_.end()) |
| + return nullptr; |
| + |
| + // If the entry's config_id does not match, or it has expired, remove it. |
| + if (it->second.config_id != device_data.config_id() || |
| + GetNow() >= it->second.expire_time) { |
| + DVLOG(2) << "Removing invalid entry " << it->first; |
| + description_map_.erase(it); |
| + return nullptr; |
| + } |
| + |
| + // Entry is valid. |
| + return &it->second; |
| +} |
| + |
| +void DeviceDescriptionService::OnParsedDeviceDescription( |
| + const DialDeviceData& device_data, |
| + const GURL& app_url, |
| + chrome::mojom::DialDeviceDescriptionPtr parsed_device_description) { |
| + DCHECK(thread_checker_.CalledOnValidThread()); |
| + |
| + // Last callback for current utility process. Release |parser_| and |
| + // SafeDialDeviceDescriptionParser object will be destroyed after this |
| + // callback. |
| + pending_device_labels_.erase(device_data.label()); |
| + if (pending_device_labels_.empty()) { |
| + parser_.reset(); |
| + } |
| + |
| + if (!parsed_device_description) { |
| + error_cb_.Run(device_data, "Failed to parse device description xml"); |
|
mark a. foltz
2017/04/25 21:06:25
nit: XML
zhaobin
2017/04/26 18:50:04
Done.
|
| + return; |
| + } |
| + |
| + ParsedDialDeviceDescription description_data; |
| + description_data.unique_id = parsed_device_description->unique_id; |
| + description_data.friendly_name = parsed_device_description->friendly_name; |
| + description_data.model_name = parsed_device_description->model_name; |
| + description_data.app_url = app_url; |
| + |
| + auto error = ValidateParsedDeviceDescription( |
| + device_data.device_description_url().host(), description_data); |
| + |
| + if (error != ErrorType::NONE) { |
| + DLOG(WARNING) << "Device description failed to validate: " << error; |
| + error_cb_.Run(device_data, "Failed to process fetch result"); |
| + return; |
| + } |
| + |
| + if (description_map_.size() >= kCacheMaxEntries) { |
| + success_cb_.Run(device_data, description_data); |
| + return; |
| + } |
| + |
| + CacheEntry cached_description_data; |
| + cached_description_data.expire_time = |
| + GetNow() + base::TimeDelta::FromMinutes(kDeviceDescriptionCacheTimeMins); |
| + cached_description_data.config_id = device_data.config_id(); |
| + cached_description_data.description_data = description_data; |
| + |
| + DVLOG(2) << "Got device description for " << device_data.label() |
| + << "... device description was: " |
| + << CachedDeviceDescriptionToString(cached_description_data); |
| + |
| + DVLOG(2) << "Caching device description for " << device_data.label(); |
| + description_map_.insert( |
| + std::make_pair(device_data.label(), cached_description_data)); |
| + |
| + success_cb_.Run(device_data, description_data); |
| +} |
| + |
| +void DeviceDescriptionService::OnDeviceDescriptionFetchComplete( |
| + const DialDeviceData& device_data, |
| + const DialDeviceDescriptionData& description_data) { |
| + DCHECK(thread_checker_.CalledOnValidThread()); |
| + |
| + if (!parser_) |
| + parser_ = base::MakeUnique<SafeDialDeviceDescriptionParser>(); |
| + |
| + parser_->Start( |
| + description_data.device_description, |
| + base::Bind(&DeviceDescriptionService::OnParsedDeviceDescription, |
| + base::Unretained(this), device_data, |
| + description_data.app_url)); |
| + |
| + DVLOG(2) << "Device description: " |
| + << ScrubDeviceDescriptionXml(description_data.device_description); |
| + device_description_fetcher_map_.erase(device_data.label()); |
| +} |
| + |
| +void DeviceDescriptionService::OnDeviceDescriptionFetchError( |
| + const DialDeviceData& device_data, |
| + const std::string& error_message) { |
| + DCHECK(thread_checker_.CalledOnValidThread()); |
| + DVLOG(2) << "OnDeviceDescriptionFetchError [label]: " << device_data.label(); |
| + |
| + error_cb_.Run(device_data, error_message); |
| + device_description_fetcher_map_.erase(device_data.label()); |
| +} |
| + |
| +base::Time DeviceDescriptionService::GetNow() { |
| + return base::Time::Now(); |
| +} |
| + |
| +DeviceDescriptionService::CacheEntry::CacheEntry() : config_id(-1) {} |
| +DeviceDescriptionService::CacheEntry::CacheEntry(const CacheEntry& other) = |
| + default; |
| +DeviceDescriptionService::CacheEntry::~CacheEntry() = default; |
| + |
| +} // namespace media_router |