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 constexpr int kDeviceDescriptionCacheTimeHours = 12; |
| 30 |
| 31 // Time interval to clean up cache entries. |
| 32 constexpr int kCacheCleanUpTimeoutMins = 30; |
| 33 |
| 34 // Maximum size on the number of cached entries. |
| 35 constexpr 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); |
| 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_cache_.size(); |
| 167 base::EraseIf(description_cache_, |
| 168 [&now](const std::pair<std::string, CacheEntry>& cache_pair) { |
| 169 return cache_pair.second.expire_time < now; |
| 170 }); |
| 171 DVLOG(2) << "After clean up, cache size: " << description_cache_.size(); |
| 172 |
| 173 if (description_cache_.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 // Existing Fetcher. |
| 185 const auto& it = device_description_fetcher_map_.find(device_data.label()); |
| 186 if (it != device_description_fetcher_map_.end()) |
| 187 return; |
| 188 |
| 189 auto device_description_fetcher = |
| 190 base::WrapUnique(new DeviceDescriptionFetcher( |
| 191 device_data.device_description_url(), request_context, |
| 192 base::BindOnce( |
| 193 &DeviceDescriptionService::OnDeviceDescriptionFetchComplete, |
| 194 base::Unretained(this), device_data), |
| 195 base::BindOnce( |
| 196 &DeviceDescriptionService::OnDeviceDescriptionFetchError, |
| 197 base::Unretained(this), device_data))); |
| 198 |
| 199 device_description_fetcher->Start(); |
| 200 device_description_fetcher_map_.insert(std::make_pair( |
| 201 device_data.label(), std::move(device_description_fetcher))); |
| 202 |
| 203 pending_device_labels_.insert(device_data.label()); |
| 204 } |
| 205 |
| 206 const DeviceDescriptionService::CacheEntry* |
| 207 DeviceDescriptionService::CheckAndUpdateCache( |
| 208 const DialDeviceData& device_data) { |
| 209 const auto& it = description_cache_.find(device_data.label()); |
| 210 if (it == description_cache_.end()) |
| 211 return nullptr; |
| 212 |
| 213 // If the entry's config_id does not match, or it has expired, remove it. |
| 214 if (it->second.config_id != device_data.config_id() || |
| 215 GetNow() >= it->second.expire_time) { |
| 216 DVLOG(2) << "Removing invalid entry " << it->first; |
| 217 description_cache_.erase(it); |
| 218 return nullptr; |
| 219 } |
| 220 |
| 221 // Entry is valid. |
| 222 return &it->second; |
| 223 } |
| 224 |
| 225 void DeviceDescriptionService::OnParsedDeviceDescription( |
| 226 const DialDeviceData& device_data, |
| 227 const GURL& app_url, |
| 228 chrome::mojom::DialDeviceDescriptionPtr parsed_device_description) { |
| 229 DCHECK(thread_checker_.CalledOnValidThread()); |
| 230 |
| 231 // Last callback for current utility process. Release |parser_| and |
| 232 // SafeDialDeviceDescriptionParser object will be destroyed after this |
| 233 // callback. |
| 234 pending_device_labels_.erase(device_data.label()); |
| 235 if (pending_device_labels_.empty()) { |
| 236 parser_.reset(); |
| 237 } |
| 238 |
| 239 if (!parsed_device_description) { |
| 240 error_cb_.Run(device_data, "Failed to parse device description XML"); |
| 241 return; |
| 242 } |
| 243 |
| 244 ParsedDialDeviceDescription description_data; |
| 245 description_data.unique_id = parsed_device_description->unique_id; |
| 246 description_data.friendly_name = parsed_device_description->friendly_name; |
| 247 description_data.model_name = parsed_device_description->model_name; |
| 248 description_data.app_url = app_url; |
| 249 |
| 250 auto error = ValidateParsedDeviceDescription( |
| 251 device_data.device_description_url().host(), description_data); |
| 252 |
| 253 if (error != ErrorType::NONE) { |
| 254 DLOG(WARNING) << "Device description failed to validate: " << error; |
| 255 error_cb_.Run(device_data, "Failed to process fetch result"); |
| 256 return; |
| 257 } |
| 258 |
| 259 if (description_cache_.size() >= kCacheMaxEntries) { |
| 260 success_cb_.Run(device_data, description_data); |
| 261 return; |
| 262 } |
| 263 |
| 264 CacheEntry cached_description_data; |
| 265 cached_description_data.expire_time = |
| 266 GetNow() + base::TimeDelta::FromHours(kDeviceDescriptionCacheTimeHours); |
| 267 cached_description_data.config_id = device_data.config_id(); |
| 268 cached_description_data.description_data = description_data; |
| 269 |
| 270 DVLOG(2) << "Got device description for " << device_data.label() |
| 271 << "... device description was: " |
| 272 << CachedDeviceDescriptionToString(cached_description_data); |
| 273 |
| 274 DVLOG(2) << "Caching device description for " << device_data.label(); |
| 275 description_cache_.insert( |
| 276 std::make_pair(device_data.label(), cached_description_data)); |
| 277 |
| 278 success_cb_.Run(device_data, description_data); |
| 279 } |
| 280 |
| 281 void DeviceDescriptionService::OnDeviceDescriptionFetchComplete( |
| 282 const DialDeviceData& device_data, |
| 283 const DialDeviceDescriptionData& description_data) { |
| 284 DCHECK(thread_checker_.CalledOnValidThread()); |
| 285 |
| 286 if (!parser_) |
| 287 parser_ = base::MakeUnique<SafeDialDeviceDescriptionParser>(); |
| 288 |
| 289 parser_->Start( |
| 290 description_data.device_description, |
| 291 base::Bind(&DeviceDescriptionService::OnParsedDeviceDescription, |
| 292 base::Unretained(this), device_data, |
| 293 description_data.app_url)); |
| 294 |
| 295 DVLOG(2) << "Device description: " |
| 296 << ScrubDeviceDescriptionXml(description_data.device_description); |
| 297 device_description_fetcher_map_.erase(device_data.label()); |
| 298 } |
| 299 |
| 300 void DeviceDescriptionService::OnDeviceDescriptionFetchError( |
| 301 const DialDeviceData& device_data, |
| 302 const std::string& error_message) { |
| 303 DCHECK(thread_checker_.CalledOnValidThread()); |
| 304 DVLOG(2) << "OnDeviceDescriptionFetchError [label]: " << device_data.label(); |
| 305 |
| 306 error_cb_.Run(device_data, error_message); |
| 307 device_description_fetcher_map_.erase(device_data.label()); |
| 308 } |
| 309 |
| 310 base::Time DeviceDescriptionService::GetNow() { |
| 311 return base::Time::Now(); |
| 312 } |
| 313 |
| 314 DeviceDescriptionService::CacheEntry::CacheEntry() : config_id(-1) {} |
| 315 DeviceDescriptionService::CacheEntry::CacheEntry(const CacheEntry& other) = |
| 316 default; |
| 317 DeviceDescriptionService::CacheEntry::~CacheEntry() = default; |
| 318 |
| 319 } // namespace media_router |
OLD | NEW |