| 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 | 
|  |