Index: chrome/browser/media/router/device_description_service.cc |
diff --git a/chrome/browser/media/router/device_description_service.cc b/chrome/browser/media/router/device_description_service.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..12bf26c30e0fd39ea2c00443957b772b69ebbae7 |
--- /dev/null |
+++ b/chrome/browser/media/router/device_description_service.cc |
@@ -0,0 +1,320 @@ |
+// Copyright 2017 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#include "chrome/browser/media/router/device_description_service.h" |
+ |
+#include "base/strings/string_util.h" |
+#include "chrome/browser/extensions/api/dial/device_description_fetcher.h" |
+#include "chrome/browser/extensions/api/dial/dial_device_data.h" |
+#include "chrome/browser/profiles/profile.h" |
+#include "content/public/browser/browser_thread.h" |
+#include "net/base/ip_address.h" |
+#include "third_party/libxml/chromium/libxml_utils.h" |
+#include "url/gurl.h" |
+ |
+using base::Time; |
+using base::TimeDelta; |
+using content::BrowserThread; |
+ |
+namespace { |
+ |
+// How long to cache a device description. |
+// TODO: Use value from chrome.dial when http://crbug.com/165289 is fixed. |
+const int kDeviceDescriptionCacheTimeMillis = 30 * 60 * 1000; |
+ |
+// Replaces "<element_name>content</element_name>" with |
+// "<element_name>***</element_name>" |
+bool Scrub(const std::string& element_name, std::string& xml_text) { |
+ size_t pos = xml_text.find("<" + element_name + ">"); |
+ size_t end_pos = xml_text.find("</" + element_name + ">"); |
+ size_t start_pos = pos + element_name.length() + 2; |
+ |
+ if (pos == std::string::npos || end_pos == std::string::npos) |
+ return false; |
+ |
+ xml_text.replace(start_pos, end_pos - start_pos, "***"); |
+ return true; |
+} |
+ |
+// Removes unique identifiers from the device description. |
+// |xml_text|: xmlDoc The device description. |
+// Returns false if <UDN> or <serialNumber> field does not exist in |xml_text|. |
+bool ScrubDeviceDescription(std::string& xml_text) { |
+ return Scrub("UDN", xml_text) && Scrub("serialNumber", xml_text); |
+} |
+ |
+// Validates whether a URL is valid for fetching device descriptions. |
+// |descriptionUrl|: Device description URL. |
+// Returns true if descriptionUrl is valid. |
+static bool IsValidDeviceDescriptionUrl(const GURL& device_description_url) { |
+ if (!device_description_url.SchemeIs(url::kHttpScheme)) |
+ return false; |
+ |
+ net::IPAddress address; |
+ if (!net::ParseURLHostnameToAddress(device_description_url.host(), &address)) |
+ return false; |
+ |
+ return address.IsReserved(); |
+} |
+ |
+static bool IsValidAppUrl(const GURL& app_url, const std::string& ip_address) { |
+ return app_url.SchemeIs(url::kHttpScheme) && app_url.host() != ip_address; |
+} |
+ |
+static std::string Validate( |
+ const media_router::DialDeviceDescription& description) { |
+ if (description.device_label.empty()) { |
+ return "Missing deviceLabel"; |
+ } |
+ if (description.unique_id.empty()) { |
+ return "Missing uniqueId"; |
+ } |
+ if (description.friendly_name.empty()) { |
+ return "Missing friendlyName"; |
+ } |
+ if (description.ip_address.empty()) { |
+ return "Missing ipAddress"; |
+ } |
+ if (!description.app_url.is_valid()) { |
+ return "Missing appUrl"; |
+ } |
+ if (description.fetch_time_millis.is_null()) { |
+ return "Missing fetchTimeMillis"; |
+ } |
+ if (description.fetch_time_millis.is_null()) { |
+ return "Missing expireTimeMillis"; |
+ } |
+ if (IsValidAppUrl(description.app_url, description.ip_address)) { |
+ return "Invalid appUrl"; |
+ } |
+ return ""; |
+} |
+ |
+static std::string ToString( |
+ const media_router::DialDeviceDescription& description) { |
+ std::stringstream ss; |
+ ss << "DialDeviceDescription [unique_id]: " << description.unique_id |
+ << " [device_label]: " << description.device_label |
+ << " [friendly_name]: " << description.friendly_name |
+ << " [ip_address]: " << description.ip_address |
+ << " [app_url]: " << description.app_url |
+ << " [fetch_time_millis]: " << description.fetch_time_millis |
+ << " [expire_time_millis]: " << description.expire_time_millis |
+ << " [device_type]: " << description.device_type |
+ << " [model_name]: " << description.model_name |
+ << " [config_id]: " << *description.config_id; |
+ |
+ return ss.str(); |
+} |
+ |
+} // namespace |
+ |
+namespace media_router { |
+ |
+DialDeviceDescription::DialDeviceDescription() = default; |
+DialDeviceDescription::~DialDeviceDescription() = default; |
+DialDeviceDescription::DialDeviceDescription( |
+ const DialDeviceDescription& other) = default; |
+ |
+DeviceDescriptionService::DeviceDescriptionService(Observer* observer) |
+ : observer_(observer) { |
+ DCHECK_CURRENTLY_ON(BrowserThread::IO); |
+ DCHECK(observer_); |
+} |
+ |
+DeviceDescriptionService::~DeviceDescriptionService() = default; |
+ |
+bool DeviceDescriptionService::GetDeviceDescription( |
+ const DialDeviceData& device, |
+ net::URLRequestContextGetter* request_context, |
+ DialDeviceDescription* out_description) { |
+ DCHECK_CURRENTLY_ON(BrowserThread::IO); |
+ |
+ if (CheckAndUpdateCache(device, out_description)) |
+ return true; |
+ |
+ FetchDeviceDescription(device, request_context); |
+ return false; |
+} |
+ |
+bool DeviceDescriptionService::MayStopDeviceDescriptionFetching( |
+ const std::string& device_label) { |
+ return device_description_fetcher_map_.erase(device_label) > 0; |
+} |
+ |
+void DeviceDescriptionService::FetchDeviceDescription( |
+ const DialDeviceData& dial_device, |
+ net::URLRequestContextGetter* request_context) { |
+ DCHECK_CURRENTLY_ON(BrowserThread::IO); |
+ |
+ if (!IsValidDeviceDescriptionUrl(dial_device.device_description_url())) { |
+ std::string error_message = "Invalid device description URL: " + |
+ dial_device.device_description_url().spec(); |
+ OnDeviceDescriptionFetchError(dial_device, error_message); |
+ return; |
+ } |
+ |
+ const auto& it = device_description_fetcher_map_.find(dial_device.label()); |
+ if (it != device_description_fetcher_map_.end()) { |
+ if (it->second->device_description_url() == |
+ dial_device.device_description_url()) { |
+ DVLOG(2) << "Pending request in progress [URL]: " |
+ << dial_device.device_description_url(); |
+ return; |
+ } |
+ // Destroy fetcher and cancel HTTP request. |
+ device_description_fetcher_map_.erase(it); |
+ } |
+ |
+ auto device_description_fetcher = |
+ base::MakeUnique<extensions::api::dial::DeviceDescriptionFetcher:: |
+ DeviceDescriptionFetcher>( |
+ dial_device.device_description_url(), request_context, |
+ BrowserThread::IO, |
+ base::BindOnce( |
+ &DeviceDescriptionService::OnDeviceDescriptionAvailable, |
+ base::Unretained(this), dial_device), |
+ base::BindOnce( |
+ &DeviceDescriptionService::OnDeviceDescriptionFetchError, |
+ base::Unretained(this), dial_device)); |
+ |
+ device_description_fetcher->Start(); |
+ device_description_fetcher_map_.insert(std::make_pair( |
+ dial_device.label(), std::move(device_description_fetcher))); |
+} |
+ |
+bool DeviceDescriptionService::CheckAndUpdateCache( |
+ const DialDeviceData& dial_device, |
+ DialDeviceDescription* out_description) { |
+ const auto& it = description_map_.find(dial_device.label()); |
+ if (it == description_map_.end()) |
+ return false; |
+ |
+ // If the entry's configId does not match, or it has expired, remove it. |
+ if (it->second.config_id != dial_device.config_id() || |
+ base::Time::Now() >= it->second.expire_time_millis) { |
+ DVLOG(2) << "Removing invalid entry " << it->second.device_label; |
+ description_map_.erase(it); |
+ return false; |
+ } |
+ // Entry is valid. |
+ (*out_description) = it->second; |
+ return true; |
+} |
+ |
+bool DeviceDescriptionService::ProcessDeviceDescription( |
+ const DialDeviceData& dial_device, |
+ const std::string& xmlText, |
+ const GURL& app_url, |
+ DialDeviceDescription* description) { |
+ if (!ParseDeviceDescription(dial_device, xmlText, app_url, description)) { |
+ DVLOG(2) << "Invalid device description"; |
+ return false; |
+ } |
+ DVLOG(2) << "Got device description for " << description->device_label; |
+ DVLOG(2) << "... device description was: " << ToString(*description); |
+ |
+ // Stick it in the cache if the description has a config (version) id. |
+ // NOTE: We could cache descriptions without a config id and rely on the |
+ // timeout to eventually update the cache. |
+ if (description->config_id) { |
+ DVLOG(2) << "Caching device description for " << description->device_label; |
+ description_map_.insert(std::make_pair(dial_device.label(), *description)); |
+ } |
+ return true; |
+} |
+ |
+bool DeviceDescriptionService::ParseDeviceDescription( |
+ const DialDeviceData& dial_device, |
+ const std::string& xml_text, |
+ const GURL& app_url, |
+ DialDeviceDescription* description) { |
+ XmlReader xml_reader; |
+ if (!xml_reader.Load(xml_text)) |
+ return false; |
+ |
+ description->fetch_time_millis = base::Time::Now(); |
+ description->expire_time_millis = |
+ description->fetch_time_millis + |
+ base::TimeDelta::FromMilliseconds(kDeviceDescriptionCacheTimeMillis); |
+ description->device_label = dial_device.label(); |
+ description->config_id = dial_device.config_id(); |
+ description->ip_address = dial_device.device_description_url().host(); |
+ description->app_url = app_url; |
+ |
+ while (xml_reader.Read()) { |
+ xml_reader.SkipToElement(); |
+ std::string node_name(xml_reader.NodeName()); |
+ |
+ if (node_name == "deviceType") { |
+ if (!xml_reader.ReadElementContent(&description->device_type)) |
+ return false; |
+ } else if (node_name == "modelName") { |
+ if (!xml_reader.ReadElementContent(&description->model_name)) |
+ return false; |
+ } else if (node_name == "friendlyName") { |
+ if (!xml_reader.ReadElementContent(&description->friendly_name)) |
+ return false; |
+ } else if (node_name == "UDN") { |
+ if (!xml_reader.ReadElementContent(&description->unique_id)) |
+ return false; |
+ } |
+ } |
+ |
+ // If friendly name does not exist, fall back to use model name + last 4 |
+ // digits of UUID as friendly name. |
+ if (description->friendly_name.empty() && !description->model_name.empty()) { |
+ description->friendly_name = description->model_name; |
+ if (!description->unique_id.empty()) { |
+ description->friendly_name += |
+ '[' + |
+ description->unique_id.substr(description->unique_id.length() - 4) + |
+ ']'; |
+ } |
+ DVLOG(2) |
+ << "Fixed device description: created friendlyName from modelName."; |
+ } |
+ |
+ std::string xml_logging = xml_text; |
+ if (ScrubDeviceDescription(xml_logging)) |
+ DVLOG(2) << "Device description: " << xml_logging; |
+ |
+ std::string error = Validate(*description); |
+ if (!error.empty()) { |
+ DLOG(WARNING) << "Device description failed to validate: " << error; |
+ return false; |
+ } |
+ |
+ return true; |
+} |
+ |
+void DeviceDescriptionService::OnDeviceDescriptionAvailable( |
+ const extensions::api::dial::DialDeviceData& dial_device, |
+ const extensions::api::dial::DialDeviceDescriptionData& description_data) { |
+ DCHECK_CURRENTLY_ON(BrowserThread::IO); |
+ |
+ DialDeviceDescription description; |
+ if (ProcessDeviceDescription(dial_device, description_data.device_description, |
+ description_data.app_url, &description)) { |
+ observer_->OnDeviceDescriptionAvailable(dial_device.label(), description); |
+ } else { |
+ observer_->OnDeviceDescriptionFetchError(dial_device.label(), |
+ "Failed to process fetch result"); |
+ } |
+ |
+ device_description_fetcher_map_.erase(dial_device.label()); |
+} |
+ |
+void DeviceDescriptionService::OnDeviceDescriptionFetchError( |
+ const extensions::api::dial::DialDeviceData& dial_device, |
+ const std::string& error_message) { |
+ DCHECK_CURRENTLY_ON(BrowserThread::IO); |
+ |
+ DVLOG(2) << "OnDeviceDescriptionFetchError [label]: " << dial_device.label(); |
+ |
+ observer_->OnDeviceDescriptionFetchError(dial_device.label(), error_message); |
+ device_description_fetcher_map_.erase(dial_device.label()); |
+} |
+ |
+} // namespace media_router |