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/device_description_service.h" |
| 6 |
| 7 #include "base/strings/string_util.h" |
| 8 #include "chrome/browser/extensions/api/dial/device_description_fetcher.h" |
| 9 #include "chrome/browser/extensions/api/dial/dial_device_data.h" |
| 10 #include "chrome/browser/profiles/profile.h" |
| 11 #include "content/public/browser/browser_thread.h" |
| 12 #include "net/base/ip_address.h" |
| 13 #include "third_party/libxml/chromium/libxml_utils.h" |
| 14 #include "url/gurl.h" |
| 15 |
| 16 using base::Time; |
| 17 using base::TimeDelta; |
| 18 using content::BrowserThread; |
| 19 |
| 20 namespace { |
| 21 |
| 22 // How long to cache a device description. |
| 23 // TODO: Use value from chrome.dial when http://crbug.com/165289 is fixed. |
| 24 const int kDeviceDescriptionCacheTimeMillis = 30 * 60 * 1000; |
| 25 |
| 26 // Replaces "<element_name>content</element_name>" with |
| 27 // "<element_name>***</element_name>" |
| 28 bool Scrub(const std::string& element_name, std::string& xml_text) { |
| 29 size_t pos = xml_text.find("<" + element_name + ">"); |
| 30 size_t end_pos = xml_text.find("</" + element_name + ">"); |
| 31 size_t start_pos = pos + element_name.length() + 2; |
| 32 |
| 33 if (pos == std::string::npos || end_pos == std::string::npos) |
| 34 return false; |
| 35 |
| 36 xml_text.replace(start_pos, end_pos - start_pos, "***"); |
| 37 return true; |
| 38 } |
| 39 |
| 40 // Removes unique identifiers from the device description. |
| 41 // |xml_text|: xmlDoc The device description. |
| 42 // Returns false if <UDN> or <serialNumber> field does not exist in |xml_text|. |
| 43 bool ScrubDeviceDescription(std::string& xml_text) { |
| 44 return Scrub("UDN", xml_text) && Scrub("serialNumber", xml_text); |
| 45 } |
| 46 |
| 47 // Validates whether a URL is valid for fetching device descriptions. |
| 48 // |descriptionUrl|: Device description URL. |
| 49 // Returns true if descriptionUrl is valid. |
| 50 static bool IsValidDeviceDescriptionUrl(const GURL& device_description_url) { |
| 51 if (!device_description_url.SchemeIs(url::kHttpScheme)) |
| 52 return false; |
| 53 |
| 54 net::IPAddress address; |
| 55 if (!net::ParseURLHostnameToAddress(device_description_url.host(), &address)) |
| 56 return false; |
| 57 |
| 58 return address.IsReserved(); |
| 59 } |
| 60 |
| 61 static bool IsValidAppUrl(const GURL& app_url, const std::string& ip_address) { |
| 62 return app_url.SchemeIs(url::kHttpScheme) && app_url.host() == ip_address; |
| 63 } |
| 64 |
| 65 static std::string Validate( |
| 66 const media_router::DialDeviceDescription& description) { |
| 67 if (description.device_label.empty()) { |
| 68 return "Missing deviceLabel"; |
| 69 } |
| 70 if (description.unique_id.empty()) { |
| 71 return "Missing uniqueId"; |
| 72 } |
| 73 if (description.friendly_name.empty()) { |
| 74 return "Missing friendlyName"; |
| 75 } |
| 76 if (description.ip_address.empty()) { |
| 77 return "Missing ipAddress"; |
| 78 } |
| 79 if (!description.app_url.is_valid()) { |
| 80 return "Missing appUrl"; |
| 81 } |
| 82 if (description.fetch_time_millis.is_null()) { |
| 83 return "Missing fetchTimeMillis"; |
| 84 } |
| 85 if (description.fetch_time_millis.is_null()) { |
| 86 return "Missing expireTimeMillis"; |
| 87 } |
| 88 if (IsValidAppUrl(description.app_url, description.ip_address)) { |
| 89 return "Invalid appUrl"; |
| 90 } |
| 91 return ""; |
| 92 } |
| 93 |
| 94 static std::string ToString( |
| 95 const media_router::DialDeviceDescription& description) { |
| 96 std::stringstream ss; |
| 97 ss << "DialDeviceDescription [unique_id]: " << description.unique_id |
| 98 << " [device_label]: " << description.device_label |
| 99 << " [friendly_name]: " << description.friendly_name |
| 100 << " [ip_address]: " << description.ip_address |
| 101 << " [app_url]: " << description.app_url |
| 102 << " [fetch_time_millis]: " << description.fetch_time_millis |
| 103 << " [expire_time_millis]: " << description.expire_time_millis |
| 104 << " [device_type]: " << description.device_type |
| 105 << " [model_name]: " << description.model_name |
| 106 << " [config_id]: " << *description.config_id; |
| 107 |
| 108 return ss.str(); |
| 109 } |
| 110 |
| 111 } // namespace |
| 112 |
| 113 namespace media_router { |
| 114 |
| 115 DialDeviceDescription::DialDeviceDescription() = default; |
| 116 DialDeviceDescription::~DialDeviceDescription() = default; |
| 117 DialDeviceDescription::DialDeviceDescription( |
| 118 const DialDeviceDescription& other) = default; |
| 119 |
| 120 DeviceDescriptionService::DeviceDescriptionService(Observer* observer) |
| 121 : observer_(observer) { |
| 122 DCHECK(observer_); |
| 123 } |
| 124 |
| 125 DeviceDescriptionService::~DeviceDescriptionService() = default; |
| 126 |
| 127 bool DeviceDescriptionService::GetDeviceDescription( |
| 128 const DialDeviceData& device, |
| 129 net::URLRequestContextGetter* request_context, |
| 130 DialDeviceDescription* out_description) { |
| 131 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 132 |
| 133 if (CheckAndUpdateCache(device, out_description)) |
| 134 return true; |
| 135 |
| 136 FetchDeviceDescription(device, request_context); |
| 137 return false; |
| 138 } |
| 139 |
| 140 bool DeviceDescriptionService::MayStopDeviceDescriptionFetching( |
| 141 const std::string& device_label) { |
| 142 return device_description_fetcher_map_.erase(device_label) > 0; |
| 143 } |
| 144 |
| 145 void DeviceDescriptionService::CheckAccess( |
| 146 const GURL& device_description_url, |
| 147 net::URLRequestContextGetter* request_context, |
| 148 const CheckAccessCallback& check_access_callback) { |
| 149 const auto& it = pending_access_requests_.find(device_description_url); |
| 150 if (it != pending_access_requests_.end()) |
| 151 return; |
| 152 |
| 153 auto device_description_fetcher = |
| 154 base::MakeUnique<extensions::api::dial::DeviceDescriptionFetcher>( |
| 155 device_description_url, request_context, |
| 156 base::BindOnce(&DeviceDescriptionService::OnCheckAccessSucceeded, |
| 157 base::Unretained(this), check_access_callback), |
| 158 base::BindOnce(&DeviceDescriptionService::OnCheckAccessError, |
| 159 base::Unretained(this), check_access_callback)); |
| 160 |
| 161 device_description_fetcher->Start(); |
| 162 pending_access_requests_.insert(std::make_pair( |
| 163 device_description_url, std::move(device_description_fetcher))); |
| 164 } |
| 165 |
| 166 void DeviceDescriptionService::OnCheckAccessSucceeded( |
| 167 const CheckAccessCallback& check_access_cb, |
| 168 const DialDeviceDescriptionData& description_data) { |
| 169 check_access_cb.Run(true); |
| 170 } |
| 171 |
| 172 void DeviceDescriptionService::OnCheckAccessError( |
| 173 const CheckAccessCallback& check_access_cb, |
| 174 const std::string& error_message) { |
| 175 check_access_cb.Run(false); |
| 176 } |
| 177 |
| 178 void DeviceDescriptionService::FetchDeviceDescription( |
| 179 const DialDeviceData& dial_device, |
| 180 net::URLRequestContextGetter* request_context) { |
| 181 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 182 |
| 183 if (!IsValidDeviceDescriptionUrl(dial_device.device_description_url())) { |
| 184 std::string error_message = "Invalid device description URL: " + |
| 185 dial_device.device_description_url().spec(); |
| 186 OnDeviceDescriptionFetchError(dial_device, error_message); |
| 187 return; |
| 188 } |
| 189 |
| 190 const auto& it = device_description_fetcher_map_.find(dial_device.label()); |
| 191 if (it != device_description_fetcher_map_.end()) { |
| 192 if (it->second->device_description_url() == |
| 193 dial_device.device_description_url()) { |
| 194 DVLOG(2) << "Pending request in progress [URL]: " |
| 195 << dial_device.device_description_url(); |
| 196 return; |
| 197 } |
| 198 // Destroy fetcher and cancel HTTP request. |
| 199 device_description_fetcher_map_.erase(it); |
| 200 } |
| 201 |
| 202 auto device_description_fetcher = |
| 203 base::MakeUnique<extensions::api::dial::DeviceDescriptionFetcher>( |
| 204 dial_device.device_description_url(), request_context, |
| 205 base::BindOnce( |
| 206 &DeviceDescriptionService::OnDeviceDescriptionAvailable, |
| 207 base::Unretained(this), dial_device), |
| 208 base::BindOnce( |
| 209 &DeviceDescriptionService::OnDeviceDescriptionFetchError, |
| 210 base::Unretained(this), dial_device)); |
| 211 |
| 212 device_description_fetcher->Start(); |
| 213 device_description_fetcher_map_.insert(std::make_pair( |
| 214 dial_device.label(), std::move(device_description_fetcher))); |
| 215 } |
| 216 |
| 217 bool DeviceDescriptionService::CheckAndUpdateCache( |
| 218 const DialDeviceData& dial_device, |
| 219 DialDeviceDescription* out_description) { |
| 220 const auto& it = description_map_.find(dial_device.label()); |
| 221 if (it == description_map_.end()) |
| 222 return false; |
| 223 |
| 224 // If the entry's configId does not match, or it has expired, remove it. |
| 225 if (it->second.config_id != dial_device.config_id() || |
| 226 base::Time::Now() >= it->second.expire_time_millis) { |
| 227 DVLOG(2) << "Removing invalid entry " << it->second.device_label; |
| 228 description_map_.erase(it); |
| 229 return false; |
| 230 } |
| 231 // Entry is valid. |
| 232 (*out_description) = it->second; |
| 233 return true; |
| 234 } |
| 235 |
| 236 bool DeviceDescriptionService::ProcessDeviceDescription( |
| 237 const DialDeviceData& dial_device, |
| 238 const std::string& xmlText, |
| 239 const GURL& app_url, |
| 240 DialDeviceDescription* description) { |
| 241 if (!ParseDeviceDescription(dial_device, xmlText, app_url, description)) { |
| 242 DVLOG(2) << "Invalid device description"; |
| 243 return false; |
| 244 } |
| 245 description->device_description_url = dial_device.device_description_url(); |
| 246 |
| 247 DVLOG(2) << "Got device description for " << description->device_label; |
| 248 DVLOG(2) << "... device description was: " << ToString(*description); |
| 249 |
| 250 // Stick it in the cache if the description has a config (version) id. |
| 251 // NOTE: We could cache descriptions without a config id and rely on the |
| 252 // timeout to eventually update the cache. |
| 253 if (description->config_id) { |
| 254 DVLOG(2) << "Caching device description for " << description->device_label; |
| 255 description_map_.insert(std::make_pair(dial_device.label(), *description)); |
| 256 } |
| 257 return true; |
| 258 } |
| 259 |
| 260 bool DeviceDescriptionService::ParseDeviceDescription( |
| 261 const DialDeviceData& dial_device, |
| 262 const std::string& xml_text, |
| 263 const GURL& app_url, |
| 264 DialDeviceDescription* description) { |
| 265 XmlReader xml_reader; |
| 266 if (!xml_reader.Load(xml_text)) |
| 267 return false; |
| 268 |
| 269 description->fetch_time_millis = base::Time::Now(); |
| 270 description->expire_time_millis = |
| 271 description->fetch_time_millis + |
| 272 base::TimeDelta::FromMilliseconds(kDeviceDescriptionCacheTimeMillis); |
| 273 description->device_label = dial_device.label(); |
| 274 description->config_id = dial_device.config_id(); |
| 275 description->ip_address = dial_device.device_description_url().host(); |
| 276 description->app_url = app_url; |
| 277 |
| 278 while (xml_reader.Read()) { |
| 279 xml_reader.SkipToElement(); |
| 280 std::string node_name(xml_reader.NodeName()); |
| 281 |
| 282 if (node_name == "deviceType") { |
| 283 if (!xml_reader.ReadElementContent(&description->device_type)) |
| 284 return false; |
| 285 } else if (node_name == "modelName") { |
| 286 if (!xml_reader.ReadElementContent(&description->model_name)) |
| 287 return false; |
| 288 } else if (node_name == "friendlyName") { |
| 289 if (!xml_reader.ReadElementContent(&description->friendly_name)) |
| 290 return false; |
| 291 } else if (node_name == "UDN") { |
| 292 if (!xml_reader.ReadElementContent(&description->unique_id)) |
| 293 return false; |
| 294 } |
| 295 } |
| 296 |
| 297 // If friendly name does not exist, fall back to use model name + last 4 |
| 298 // digits of UUID as friendly name. |
| 299 if (description->friendly_name.empty() && !description->model_name.empty()) { |
| 300 description->friendly_name = description->model_name; |
| 301 if (!description->unique_id.empty()) { |
| 302 description->friendly_name += |
| 303 '[' + |
| 304 description->unique_id.substr(description->unique_id.length() - 4) + |
| 305 ']'; |
| 306 } |
| 307 DVLOG(2) |
| 308 << "Fixed device description: created friendlyName from modelName."; |
| 309 } |
| 310 |
| 311 std::string xml_logging = xml_text; |
| 312 if (ScrubDeviceDescription(xml_logging)) |
| 313 DVLOG(2) << "Device description: " << xml_logging; |
| 314 |
| 315 std::string error = Validate(*description); |
| 316 if (!error.empty()) { |
| 317 DLOG(WARNING) << "Device description failed to validate: " << error; |
| 318 return false; |
| 319 } |
| 320 |
| 321 return true; |
| 322 } |
| 323 |
| 324 void DeviceDescriptionService::OnDeviceDescriptionAvailable( |
| 325 const extensions::api::dial::DialDeviceData& dial_device, |
| 326 const extensions::api::dial::DialDeviceDescriptionData& description_data) { |
| 327 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 328 |
| 329 DialDeviceDescription description; |
| 330 if (ProcessDeviceDescription(dial_device, description_data.device_description, |
| 331 description_data.app_url, &description)) { |
| 332 observer_->OnDeviceDescriptionAvailable(dial_device.label(), description); |
| 333 } else { |
| 334 observer_->OnDeviceDescriptionFetchError(dial_device.label(), |
| 335 "Failed to process fetch result"); |
| 336 } |
| 337 |
| 338 device_description_fetcher_map_.erase(dial_device.label()); |
| 339 } |
| 340 |
| 341 void DeviceDescriptionService::OnDeviceDescriptionFetchError( |
| 342 const extensions::api::dial::DialDeviceData& dial_device, |
| 343 const std::string& error_message) { |
| 344 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 345 |
| 346 DVLOG(2) << "OnDeviceDescriptionFetchError [label]: " << dial_device.label(); |
| 347 |
| 348 observer_->OnDeviceDescriptionFetchError(dial_device.label(), error_message); |
| 349 device_description_fetcher_map_.erase(dial_device.label()); |
| 350 } |
| 351 |
| 352 } // namespace media_router |
OLD | NEW |