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_CURRENTLY_ON(BrowserThread::IO); |
| 123 DCHECK(observer_); |
| 124 } |
| 125 |
| 126 DeviceDescriptionService::~DeviceDescriptionService() = default; |
| 127 |
| 128 bool DeviceDescriptionService::GetDeviceDescription( |
| 129 const DialDeviceData& device, |
| 130 net::URLRequestContextGetter* request_context, |
| 131 DialDeviceDescription* out_description) { |
| 132 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 133 |
| 134 if (CheckAndUpdateCache(device, out_description)) |
| 135 return true; |
| 136 |
| 137 FetchDeviceDescription(device, request_context); |
| 138 return false; |
| 139 } |
| 140 |
| 141 bool DeviceDescriptionService::MayStopDeviceDescriptionFetching( |
| 142 const std::string& device_label) { |
| 143 return device_description_fetcher_map_.erase(device_label) > 0; |
| 144 } |
| 145 |
| 146 void DeviceDescriptionService::FetchDeviceDescription( |
| 147 const DialDeviceData& dial_device, |
| 148 net::URLRequestContextGetter* request_context) { |
| 149 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 150 |
| 151 if (!IsValidDeviceDescriptionUrl(dial_device.device_description_url())) { |
| 152 std::string error_message = "Invalid device description URL: " + |
| 153 dial_device.device_description_url().spec(); |
| 154 OnDeviceDescriptionFetchError(dial_device, error_message); |
| 155 return; |
| 156 } |
| 157 |
| 158 const auto& it = device_description_fetcher_map_.find(dial_device.label()); |
| 159 if (it != device_description_fetcher_map_.end()) { |
| 160 if (it->second->device_description_url() == |
| 161 dial_device.device_description_url()) { |
| 162 DVLOG(2) << "Pending request in progress [URL]: " |
| 163 << dial_device.device_description_url(); |
| 164 return; |
| 165 } |
| 166 // Destroy fetcher and cancel HTTP request. |
| 167 device_description_fetcher_map_.erase(it); |
| 168 } |
| 169 |
| 170 auto device_description_fetcher = |
| 171 base::MakeUnique<extensions::api::dial::DeviceDescriptionFetcher:: |
| 172 DeviceDescriptionFetcher>( |
| 173 dial_device.device_description_url(), request_context, |
| 174 BrowserThread::IO, |
| 175 base::BindOnce( |
| 176 &DeviceDescriptionService::OnDeviceDescriptionAvailable, |
| 177 base::Unretained(this), dial_device), |
| 178 base::BindOnce( |
| 179 &DeviceDescriptionService::OnDeviceDescriptionFetchError, |
| 180 base::Unretained(this), dial_device)); |
| 181 |
| 182 device_description_fetcher->Start(); |
| 183 device_description_fetcher_map_.insert(std::make_pair( |
| 184 dial_device.label(), std::move(device_description_fetcher))); |
| 185 } |
| 186 |
| 187 bool DeviceDescriptionService::CheckAndUpdateCache( |
| 188 const DialDeviceData& dial_device, |
| 189 DialDeviceDescription* out_description) { |
| 190 const auto& it = description_map_.find(dial_device.label()); |
| 191 if (it == description_map_.end()) |
| 192 return false; |
| 193 |
| 194 // If the entry's configId does not match, or it has expired, remove it. |
| 195 if (it->second.config_id != dial_device.config_id() || |
| 196 base::Time::Now() >= it->second.expire_time_millis) { |
| 197 DVLOG(2) << "Removing invalid entry " << it->second.device_label; |
| 198 description_map_.erase(it); |
| 199 return false; |
| 200 } |
| 201 // Entry is valid. |
| 202 (*out_description) = it->second; |
| 203 return true; |
| 204 } |
| 205 |
| 206 bool DeviceDescriptionService::ProcessDeviceDescription( |
| 207 const DialDeviceData& dial_device, |
| 208 const std::string& xmlText, |
| 209 const GURL& app_url, |
| 210 DialDeviceDescription* description) { |
| 211 if (!ParseDeviceDescription(dial_device, xmlText, app_url, description)) { |
| 212 DVLOG(2) << "Invalid device description"; |
| 213 return false; |
| 214 } |
| 215 DVLOG(2) << "Got device description for " << description->device_label; |
| 216 DVLOG(2) << "... device description was: " << ToString(*description); |
| 217 |
| 218 // Stick it in the cache if the description has a config (version) id. |
| 219 // NOTE: We could cache descriptions without a config id and rely on the |
| 220 // timeout to eventually update the cache. |
| 221 if (description->config_id) { |
| 222 DVLOG(2) << "Caching device description for " << description->device_label; |
| 223 description_map_.insert(std::make_pair(dial_device.label(), *description)); |
| 224 } |
| 225 return true; |
| 226 } |
| 227 |
| 228 bool DeviceDescriptionService::ParseDeviceDescription( |
| 229 const DialDeviceData& dial_device, |
| 230 const std::string& xml_text, |
| 231 const GURL& app_url, |
| 232 DialDeviceDescription* description) { |
| 233 XmlReader xml_reader; |
| 234 if (!xml_reader.Load(xml_text)) |
| 235 return false; |
| 236 |
| 237 description->fetch_time_millis = base::Time::Now(); |
| 238 description->expire_time_millis = |
| 239 description->fetch_time_millis + |
| 240 base::TimeDelta::FromMilliseconds(kDeviceDescriptionCacheTimeMillis); |
| 241 description->device_label = dial_device.label(); |
| 242 description->config_id = dial_device.config_id(); |
| 243 description->ip_address = dial_device.device_description_url().host(); |
| 244 description->app_url = app_url; |
| 245 |
| 246 while (xml_reader.Read()) { |
| 247 xml_reader.SkipToElement(); |
| 248 std::string node_name(xml_reader.NodeName()); |
| 249 |
| 250 if (node_name == "deviceType") { |
| 251 if (!xml_reader.ReadElementContent(&description->device_type)) |
| 252 return false; |
| 253 } else if (node_name == "modelName") { |
| 254 if (!xml_reader.ReadElementContent(&description->model_name)) |
| 255 return false; |
| 256 } else if (node_name == "friendlyName") { |
| 257 if (!xml_reader.ReadElementContent(&description->friendly_name)) |
| 258 return false; |
| 259 } else if (node_name == "UDN") { |
| 260 if (!xml_reader.ReadElementContent(&description->unique_id)) |
| 261 return false; |
| 262 } |
| 263 } |
| 264 |
| 265 // If friendly name does not exist, fall back to use model name + last 4 |
| 266 // digits of UUID as friendly name. |
| 267 if (description->friendly_name.empty() && !description->model_name.empty()) { |
| 268 description->friendly_name = description->model_name; |
| 269 if (!description->unique_id.empty()) { |
| 270 description->friendly_name += |
| 271 '[' + |
| 272 description->unique_id.substr(description->unique_id.length() - 4) + |
| 273 ']'; |
| 274 } |
| 275 DVLOG(2) |
| 276 << "Fixed device description: created friendlyName from modelName."; |
| 277 } |
| 278 |
| 279 std::string xml_logging = xml_text; |
| 280 if (ScrubDeviceDescription(xml_logging)) |
| 281 DVLOG(2) << "Device description: " << xml_logging; |
| 282 |
| 283 std::string error = Validate(*description); |
| 284 if (!error.empty()) { |
| 285 DLOG(WARNING) << "Device description failed to validate: " << error; |
| 286 return false; |
| 287 } |
| 288 |
| 289 return true; |
| 290 } |
| 291 |
| 292 void DeviceDescriptionService::OnDeviceDescriptionAvailable( |
| 293 const extensions::api::dial::DialDeviceData& dial_device, |
| 294 const extensions::api::dial::DialDeviceDescriptionData& description_data) { |
| 295 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 296 |
| 297 DialDeviceDescription description; |
| 298 if (ProcessDeviceDescription(dial_device, description_data.device_description, |
| 299 description_data.app_url, &description)) { |
| 300 observer_->OnDeviceDescriptionAvailable(dial_device.label(), description); |
| 301 } else { |
| 302 observer_->OnDeviceDescriptionFetchError(dial_device.label(), |
| 303 "Failed to process fetch result"); |
| 304 } |
| 305 |
| 306 device_description_fetcher_map_.erase(dial_device.label()); |
| 307 } |
| 308 |
| 309 void DeviceDescriptionService::OnDeviceDescriptionFetchError( |
| 310 const extensions::api::dial::DialDeviceData& dial_device, |
| 311 const std::string& error_message) { |
| 312 DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| 313 |
| 314 DVLOG(2) << "OnDeviceDescriptionFetchError [label]: " << dial_device.label(); |
| 315 |
| 316 observer_->OnDeviceDescriptionFetchError(dial_device.label(), error_message); |
| 317 device_description_fetcher_map_.erase(dial_device.label()); |
| 318 } |
| 319 |
| 320 } // namespace media_router |
OLD | NEW |