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