Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #include "chrome/browser/media/router/discovery/dial/device_description_service. h" | |
| 6 | |
| 7 #include "base/stl_util.h" | |
| 8 #include "base/strings/string_util.h" | |
| 9 #include "chrome/browser/media/router/discovery/dial/device_description_fetcher. h" | |
| 10 #include "chrome/browser/media/router/discovery/dial/safe_dial_device_descriptio n_parser.h" | |
| 11 #include "net/base/ip_address.h" | |
| 12 #include "url/gurl.h" | |
| 13 | |
| 14 namespace { | |
| 15 | |
| 16 enum ErrorType { | |
| 17 NONE, | |
| 18 MISSING_UNIQUE_ID, | |
| 19 MISSING_FRIENDLY_NAME, | |
| 20 MISSING_APP_URL, | |
| 21 INVALID_APP_URL | |
| 22 }; | |
| 23 | |
| 24 // How long to cache a device description. | |
| 25 const int kDeviceDescriptionCacheTimeMillis = 30 * 60 * 1000; | |
|
mark a. foltz
2017/04/18 18:16:26
Per my earlier comment this could be much longer I
zhaobin
2017/04/21 23:12:58
Done.
| |
| 26 | |
| 27 // Time interval to clean up cache entries. | |
| 28 const int kCacheCleanUpTimeoutMins = 30; | |
| 29 | |
| 30 // Maximum size on the number of cached entries. | |
| 31 const int kCacheLimit = 256; | |
|
mark a. foltz
2017/04/18 18:16:26
kCacheMaxEntries ?
zhaobin
2017/04/21 23:12:57
Done.
| |
| 32 | |
| 33 #ifndef NDEBUG | |
| 34 // Replaces "<element_name>content</element_name>" with | |
| 35 // "<element_name>***</element_name>" | |
| 36 void Scrub(const std::string& element_name, std::string* xml_text) { | |
| 37 size_t pos = xml_text->find("<" + element_name + ">"); | |
| 38 size_t end_pos = xml_text->find("</" + element_name + ">"); | |
| 39 | |
| 40 if (pos == std::string::npos || end_pos == std::string::npos) | |
| 41 return; | |
| 42 | |
| 43 size_t start_pos = pos + element_name.length() + 2; | |
| 44 if (end_pos > start_pos) | |
| 45 xml_text->replace(start_pos, end_pos - start_pos, "***"); | |
| 46 } | |
| 47 | |
| 48 // Removes unique identifiers from the device description. | |
| 49 // |xml_text|: The device description XML. | |
| 50 // Returns original XML text if <UDN> or <serialNumber> field does not exist. | |
| 51 std::string ScrubDeviceDescriptionXml(const std::string& xml_text) { | |
| 52 std::string scrubbed_xml(xml_text); | |
| 53 Scrub("UDN", &scrubbed_xml); | |
| 54 Scrub("serialNumber", &scrubbed_xml); | |
| 55 return scrubbed_xml; | |
| 56 } | |
| 57 | |
| 58 std::string CachedDeviceDescriptionToString( | |
| 59 const media_router::DeviceDescriptionService::CacheEntry& cached_data) { | |
| 60 std::stringstream ss; | |
|
mark a. foltz
2017/04/18 18:16:26
#ifndef NDEBUG
#include <sstream>
#endif
zhaobin
2017/04/21 23:12:58
Done.
| |
| 61 ss << "CachedDialDeviceDescription [unique_id]: " | |
| 62 << cached_data.description_data.unique_id | |
| 63 << " [friendly_name]: " << cached_data.description_data.friendly_name | |
| 64 << " [model_name]: " << cached_data.description_data.model_name | |
| 65 << " [app_url]: " << cached_data.description_data.app_url | |
| 66 << " [expire_time]: " << cached_data.expire_time | |
| 67 << " [config_id]: " << cached_data.config_id; | |
| 68 | |
| 69 return ss.str(); | |
| 70 } | |
| 71 #endif | |
| 72 | |
| 73 bool IsValidAppUrl(const GURL& app_url, const std::string& ip_address) { | |
| 74 return app_url.SchemeIs(url::kHttpScheme) && app_url.host() == ip_address; | |
| 75 } | |
| 76 | |
| 77 // Checks mandatory fields. Returns ErrorType::NONE if device description is | |
| 78 // valid; Otherwise returns specific error type. | |
| 79 ErrorType ValidateParsedDeviceDescription( | |
| 80 const std::string& expected_ip_address, | |
| 81 const media_router::ParsedDialDeviceDescription& description_data) { | |
| 82 if (description_data.unique_id.empty()) { | |
| 83 return ErrorType::MISSING_UNIQUE_ID; | |
| 84 } | |
| 85 if (description_data.friendly_name.empty()) { | |
| 86 return ErrorType::MISSING_FRIENDLY_NAME; | |
| 87 } | |
| 88 if (!description_data.app_url.is_valid()) { | |
| 89 return ErrorType::MISSING_APP_URL; | |
| 90 } | |
| 91 if (!IsValidAppUrl(description_data.app_url, expected_ip_address)) { | |
| 92 return ErrorType::INVALID_APP_URL; | |
| 93 } | |
| 94 return ErrorType::NONE; | |
| 95 } | |
| 96 | |
| 97 } // namespace | |
| 98 | |
| 99 namespace media_router { | |
| 100 | |
| 101 DeviceDescriptionService::DeviceDescriptionService( | |
| 102 const DeviceDescriptionParseSuccessCallback& success_cb, | |
| 103 const DeviceDescriptionParseErrorCallback& error_cb) | |
| 104 : success_cb_(success_cb), error_cb_(error_cb) {} | |
| 105 | |
| 106 DeviceDescriptionService::~DeviceDescriptionService() = default; | |
| 107 | |
| 108 void DeviceDescriptionService::GetDeviceDescriptions( | |
| 109 const std::vector<DialDeviceData>& devices, | |
| 110 net::URLRequestContextGetter* request_context) { | |
| 111 DCHECK(thread_checker_.CalledOnValidThread()); | |
| 112 | |
| 113 std::map<std::string, std::unique_ptr<DeviceDescriptionFetcher>> | |
| 114 existing_fetcher_map; | |
| 115 for (auto& fetcher_it : device_description_fetcher_map_) { | |
| 116 std::string device_label = fetcher_it.first; | |
| 117 const auto& device_it = | |
| 118 std::find_if(devices.begin(), devices.end(), | |
| 119 [&device_label](const DialDeviceData& device_data) { | |
| 120 return device_data.label() == device_label; | |
| 121 }); | |
| 122 if (device_it == devices.end() || | |
| 123 device_it->device_description_url() == | |
| 124 fetcher_it.second->device_description_url()) { | |
| 125 existing_fetcher_map.insert( | |
| 126 std::make_pair(device_label, std::move(fetcher_it.second))); | |
| 127 } | |
| 128 } | |
| 129 | |
| 130 // Remove all out dated fetchers. | |
| 131 device_description_fetcher_map_ = std::move(existing_fetcher_map); | |
| 132 | |
| 133 for (const auto& device_data : devices) { | |
| 134 auto* cache_entry = CheckAndUpdateCache(device_data); | |
| 135 if (cache_entry) { | |
| 136 // Get device description from cache. | |
| 137 success_cb_.Run(device_data, cache_entry->description_data); | |
| 138 continue; | |
| 139 } | |
| 140 | |
| 141 FetchDeviceDescription(device_data, request_context); | |
| 142 } | |
| 143 | |
| 144 // Start a clean up timer. | |
| 145 if (!clean_up_timer_) { | |
| 146 clean_up_timer_.reset(new base::RepeatingTimer()); | |
| 147 clean_up_timer_->Start( | |
| 148 FROM_HERE, base::TimeDelta::FromMinutes(kCacheCleanUpTimeoutMins), this, | |
| 149 &DeviceDescriptionService::CleanUpCacheEntries); | |
| 150 } | |
| 151 } | |
| 152 | |
| 153 void DeviceDescriptionService::CleanUpCacheEntries() { | |
| 154 // TODO(zhaobin): instead of removing them directly, issue GET request to | |
| 155 // check if expired device descriptions are still alive. | |
|
mark a. foltz
2017/04/18 18:16:26
This is probably okay - if the device is found thr
zhaobin
2017/04/21 23:12:58
Done.
| |
| 156 DCHECK(thread_checker_.CalledOnValidThread()); | |
| 157 base::Time now = GetNow(); | |
| 158 | |
| 159 DVLOG(2) << "Before clean up, cache size: " << description_map_.size(); | |
| 160 base::EraseIf(description_map_, | |
| 161 [&now](const std::pair<std::string, CacheEntry>& cache_pair) { | |
| 162 return cache_pair.second.expire_time < now; | |
| 163 }); | |
| 164 DVLOG(2) << "After clean up, cache size: " << description_map_.size(); | |
|
mark a. foltz
2017/04/18 18:16:26
If the cache is emptied and there are no pending r
zhaobin
2017/04/21 23:12:58
Done.
| |
| 165 } | |
| 166 | |
| 167 base::Time DeviceDescriptionService::GetNow() { | |
|
mark a. foltz
2017/04/18 18:16:26
Is this for unit testing?
zhaobin
2017/04/21 23:12:58
Yes.
| |
| 168 return base::Time::Now(); | |
| 169 } | |
| 170 | |
| 171 void DeviceDescriptionService::FetchDeviceDescription( | |
| 172 const DialDeviceData& device_data, | |
| 173 net::URLRequestContextGetter* request_context) { | |
| 174 DCHECK(thread_checker_.CalledOnValidThread()); | |
| 175 | |
| 176 auto device_description_fetcher = base::MakeUnique<DeviceDescriptionFetcher>( | |
| 177 device_data.device_description_url(), request_context, | |
| 178 base::BindOnce( | |
| 179 &DeviceDescriptionService::OnDeviceDescriptionFetchComplete, | |
| 180 base::Unretained(this), device_data), | |
| 181 base::BindOnce(&DeviceDescriptionService::OnDeviceDescriptionFetchError, | |
| 182 base::Unretained(this), device_data)); | |
| 183 | |
| 184 device_description_fetcher->Start(); | |
| 185 device_description_fetcher_map_.insert(std::make_pair( | |
| 186 device_data.label(), std::move(device_description_fetcher))); | |
| 187 } | |
| 188 | |
| 189 const DeviceDescriptionService::CacheEntry* | |
| 190 DeviceDescriptionService::CheckAndUpdateCache( | |
| 191 const DialDeviceData& device_data) { | |
| 192 const auto& it = description_map_.find(device_data.label()); | |
| 193 if (it == description_map_.end()) | |
| 194 return nullptr; | |
| 195 | |
| 196 // If the entry's config_id does not match, or it has expired, remove it. | |
| 197 if (it->second.config_id != device_data.config_id() || | |
| 198 GetNow() >= it->second.expire_time) { | |
| 199 DVLOG(2) << "Removing invalid entry " << it->first; | |
| 200 description_map_.erase(it); | |
| 201 return nullptr; | |
| 202 } | |
| 203 | |
| 204 // Entry is valid. | |
| 205 return &it->second; | |
| 206 } | |
| 207 | |
| 208 void DeviceDescriptionService::OnParsedDeviceDescription( | |
| 209 const DialDeviceData& device_data, | |
| 210 const GURL& app_url, | |
| 211 chrome::mojom::DialDeviceDescriptionPtr parsed_device_description) { | |
| 212 DCHECK(thread_checker_.CalledOnValidThread()); | |
| 213 | |
| 214 // Last callback for current utility process. Release |parser_| and | |
| 215 // SafeDialDeviceDescriptionParser object will be destroyed after this | |
| 216 // callback. | |
| 217 if (parser_ && parser_->GetPendingParsingRequests() == 0) | |
|
mark a. foltz
2017/04/18 18:16:26
For a given list of devices, is it possible for on
zhaobin
2017/04/21 23:12:57
Done.
| |
| 218 parser_ = nullptr; | |
| 219 | |
| 220 if (!parsed_device_description) { | |
| 221 error_cb_.Run(device_data, "Failed to parse device description xml"); | |
| 222 return; | |
| 223 } | |
| 224 | |
| 225 ParsedDialDeviceDescription description_data; | |
| 226 description_data.unique_id = parsed_device_description->unique_id; | |
| 227 description_data.friendly_name = parsed_device_description->friendly_name; | |
| 228 description_data.model_name = parsed_device_description->model_name; | |
| 229 description_data.app_url = app_url; | |
| 230 | |
| 231 auto error = ValidateParsedDeviceDescription( | |
| 232 device_data.device_description_url().host(), description_data); | |
| 233 | |
| 234 if (error != ErrorType::NONE) { | |
| 235 DLOG(WARNING) << "Device description failed to validate: " << error; | |
| 236 error_cb_.Run(device_data, "Failed to process fetch result"); | |
| 237 return; | |
| 238 } | |
| 239 | |
| 240 CacheEntry cached_description_data; | |
| 241 cached_description_data.expire_time = | |
| 242 GetNow() + | |
| 243 base::TimeDelta::FromMilliseconds(kDeviceDescriptionCacheTimeMillis); | |
|
mark a. foltz
2017/04/18 18:16:26
Can you convert the cache timeout constant to use
zhaobin
2017/04/21 23:12:58
Done.
| |
| 244 cached_description_data.config_id = device_data.config_id(); | |
| 245 cached_description_data.description_data = description_data; | |
| 246 | |
| 247 DVLOG(2) << "Got device description for " << device_data.label() | |
| 248 << "... device description was: " | |
| 249 << CachedDeviceDescriptionToString(cached_description_data); | |
| 250 | |
| 251 // Stick it in the cache if the description has a config (version) id. | |
| 252 // NOTE: We could cache descriptions without a config id and rely on the | |
|
mark a. foltz
2017/04/18 18:16:26
Yes we should do this, to avoid repeatedly request
zhaobin
2017/04/21 23:12:58
Done.
| |
| 253 // timeout to eventually update the cache. | |
| 254 if (description_map_.size() < kCacheLimit && | |
|
mark a. foltz
2017/04/18 18:16:26
If the cache size limit has been hit, you can earl
zhaobin
2017/04/21 23:12:57
Done.
| |
| 255 cached_description_data.config_id) { | |
| 256 DVLOG(2) << "Caching device description for " << device_data.label(); | |
| 257 description_map_.insert( | |
| 258 std::make_pair(device_data.label(), cached_description_data)); | |
| 259 } | |
| 260 | |
| 261 success_cb_.Run(device_data, description_data); | |
| 262 } | |
| 263 | |
| 264 void DeviceDescriptionService::OnDeviceDescriptionFetchComplete( | |
| 265 const DialDeviceData& device_data, | |
| 266 const DialDeviceDescriptionData& description_data) { | |
| 267 DCHECK(thread_checker_.CalledOnValidThread()); | |
| 268 | |
| 269 if (!parser_) | |
| 270 parser_ = new SafeDialDeviceDescriptionParser(); | |
| 271 | |
| 272 parser_->Start( | |
| 273 description_data.device_description, | |
| 274 base::Bind(&DeviceDescriptionService::OnParsedDeviceDescription, | |
| 275 base::Unretained(this), device_data, | |
| 276 description_data.app_url)); | |
| 277 | |
| 278 DVLOG(2) << "Device description: " | |
| 279 << ScrubDeviceDescriptionXml(description_data.device_description); | |
| 280 device_description_fetcher_map_.erase(device_data.label()); | |
| 281 } | |
| 282 | |
| 283 void DeviceDescriptionService::OnDeviceDescriptionFetchError( | |
| 284 const DialDeviceData& device_data, | |
| 285 const std::string& error_message) { | |
| 286 DCHECK(thread_checker_.CalledOnValidThread()); | |
| 287 DVLOG(2) << "OnDeviceDescriptionFetchError [label]: " << device_data.label(); | |
| 288 | |
| 289 error_cb_.Run(device_data, error_message); | |
| 290 device_description_fetcher_map_.erase(device_data.label()); | |
| 291 } | |
| 292 | |
| 293 DeviceDescriptionService::CacheEntry::CacheEntry() : config_id(-1) {} | |
| 294 DeviceDescriptionService::CacheEntry::CacheEntry(const CacheEntry& other) = | |
| 295 default; | |
| 296 DeviceDescriptionService::CacheEntry::~CacheEntry() = default; | |
| 297 | |
| 298 } // namespace media_router | |
| OLD | NEW |