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 #include "base/stl_util.h" | |
8 #include "base/strings/string_util.h" | |
9 #include "chrome/browser/media/router/discovery/dial/device_description_fetcher. h" | |
10 #include "chrome/browser/media/router/discovery/dial/safe_dial_device_descriptio n_parser.h" | |
11 #include "net/base/ip_address.h" | |
12 #include "url/gurl.h" | |
13 | |
14 namespace { | |
15 | |
16 enum ErrorType { | |
17 NONE, | |
18 MISSING_UNIQUE_ID, | |
19 MISSING_FRIENDLY_NAME, | |
20 MISSING_APP_URL, | |
21 INVALID_APP_URL | |
22 }; | |
23 | |
24 // How long to cache a device description. | |
25 const int kDeviceDescriptionCacheTimeMillis = 30 * 60 * 1000; | |
mark a. foltz
2017/04/18 18:16:26
Per my earlier comment this could be much longer I
zhaobin
2017/04/21 23:12:58
Done.
| |
26 | |
27 // Time interval to clean up cache entries. | |
28 const int kCacheCleanUpTimeoutMins = 30; | |
29 | |
30 // Maximum size on the number of cached entries. | |
31 const int kCacheLimit = 256; | |
mark a. foltz
2017/04/18 18:16:26
kCacheMaxEntries ?
zhaobin
2017/04/21 23:12:57
Done.
| |
32 | |
33 #ifndef NDEBUG | |
34 // Replaces "<element_name>content</element_name>" with | |
35 // "<element_name>***</element_name>" | |
36 void Scrub(const std::string& element_name, std::string* xml_text) { | |
37 size_t pos = xml_text->find("<" + element_name + ">"); | |
38 size_t end_pos = xml_text->find("</" + element_name + ">"); | |
39 | |
40 if (pos == std::string::npos || end_pos == std::string::npos) | |
41 return; | |
42 | |
43 size_t start_pos = pos + element_name.length() + 2; | |
44 if (end_pos > start_pos) | |
45 xml_text->replace(start_pos, end_pos - start_pos, "***"); | |
46 } | |
47 | |
48 // Removes unique identifiers from the device description. | |
49 // |xml_text|: The device description XML. | |
50 // Returns original XML text if <UDN> or <serialNumber> field does not exist. | |
51 std::string ScrubDeviceDescriptionXml(const std::string& xml_text) { | |
52 std::string scrubbed_xml(xml_text); | |
53 Scrub("UDN", &scrubbed_xml); | |
54 Scrub("serialNumber", &scrubbed_xml); | |
55 return scrubbed_xml; | |
56 } | |
57 | |
58 std::string CachedDeviceDescriptionToString( | |
59 const media_router::DeviceDescriptionService::CacheEntry& cached_data) { | |
60 std::stringstream ss; | |
mark a. foltz
2017/04/18 18:16:26
#ifndef NDEBUG
#include <sstream>
#endif
zhaobin
2017/04/21 23:12:58
Done.
| |
61 ss << "CachedDialDeviceDescription [unique_id]: " | |
62 << cached_data.description_data.unique_id | |
63 << " [friendly_name]: " << cached_data.description_data.friendly_name | |
64 << " [model_name]: " << cached_data.description_data.model_name | |
65 << " [app_url]: " << cached_data.description_data.app_url | |
66 << " [expire_time]: " << cached_data.expire_time | |
67 << " [config_id]: " << cached_data.config_id; | |
68 | |
69 return ss.str(); | |
70 } | |
71 #endif | |
72 | |
73 bool IsValidAppUrl(const GURL& app_url, const std::string& ip_address) { | |
74 return app_url.SchemeIs(url::kHttpScheme) && app_url.host() == ip_address; | |
75 } | |
76 | |
77 // Checks mandatory fields. Returns ErrorType::NONE if device description is | |
78 // valid; Otherwise returns specific error type. | |
79 ErrorType ValidateParsedDeviceDescription( | |
80 const std::string& expected_ip_address, | |
81 const media_router::ParsedDialDeviceDescription& description_data) { | |
82 if (description_data.unique_id.empty()) { | |
83 return ErrorType::MISSING_UNIQUE_ID; | |
84 } | |
85 if (description_data.friendly_name.empty()) { | |
86 return ErrorType::MISSING_FRIENDLY_NAME; | |
87 } | |
88 if (!description_data.app_url.is_valid()) { | |
89 return ErrorType::MISSING_APP_URL; | |
90 } | |
91 if (!IsValidAppUrl(description_data.app_url, expected_ip_address)) { | |
92 return ErrorType::INVALID_APP_URL; | |
93 } | |
94 return ErrorType::NONE; | |
95 } | |
96 | |
97 } // namespace | |
98 | |
99 namespace media_router { | |
100 | |
101 DeviceDescriptionService::DeviceDescriptionService( | |
102 const DeviceDescriptionParseSuccessCallback& success_cb, | |
103 const DeviceDescriptionParseErrorCallback& error_cb) | |
104 : success_cb_(success_cb), error_cb_(error_cb) {} | |
105 | |
106 DeviceDescriptionService::~DeviceDescriptionService() = default; | |
107 | |
108 void DeviceDescriptionService::GetDeviceDescriptions( | |
109 const std::vector<DialDeviceData>& devices, | |
110 net::URLRequestContextGetter* request_context) { | |
111 DCHECK(thread_checker_.CalledOnValidThread()); | |
112 | |
113 std::map<std::string, std::unique_ptr<DeviceDescriptionFetcher>> | |
114 existing_fetcher_map; | |
115 for (auto& fetcher_it : device_description_fetcher_map_) { | |
116 std::string device_label = fetcher_it.first; | |
117 const auto& device_it = | |
118 std::find_if(devices.begin(), devices.end(), | |
119 [&device_label](const DialDeviceData& device_data) { | |
120 return device_data.label() == device_label; | |
121 }); | |
122 if (device_it == devices.end() || | |
123 device_it->device_description_url() == | |
124 fetcher_it.second->device_description_url()) { | |
125 existing_fetcher_map.insert( | |
126 std::make_pair(device_label, std::move(fetcher_it.second))); | |
127 } | |
128 } | |
129 | |
130 // Remove all out dated fetchers. | |
131 device_description_fetcher_map_ = std::move(existing_fetcher_map); | |
132 | |
133 for (const auto& device_data : devices) { | |
134 auto* cache_entry = CheckAndUpdateCache(device_data); | |
135 if (cache_entry) { | |
136 // Get device description from cache. | |
137 success_cb_.Run(device_data, cache_entry->description_data); | |
138 continue; | |
139 } | |
140 | |
141 FetchDeviceDescription(device_data, request_context); | |
142 } | |
143 | |
144 // Start a clean up timer. | |
145 if (!clean_up_timer_) { | |
146 clean_up_timer_.reset(new base::RepeatingTimer()); | |
147 clean_up_timer_->Start( | |
148 FROM_HERE, base::TimeDelta::FromMinutes(kCacheCleanUpTimeoutMins), this, | |
149 &DeviceDescriptionService::CleanUpCacheEntries); | |
150 } | |
151 } | |
152 | |
153 void DeviceDescriptionService::CleanUpCacheEntries() { | |
154 // TODO(zhaobin): instead of removing them directly, issue GET request to | |
155 // check if expired device descriptions are still alive. | |
mark a. foltz
2017/04/18 18:16:26
This is probably okay - if the device is found thr
zhaobin
2017/04/21 23:12:58
Done.
| |
156 DCHECK(thread_checker_.CalledOnValidThread()); | |
157 base::Time now = GetNow(); | |
158 | |
159 DVLOG(2) << "Before clean up, cache size: " << description_map_.size(); | |
160 base::EraseIf(description_map_, | |
161 [&now](const std::pair<std::string, CacheEntry>& cache_pair) { | |
162 return cache_pair.second.expire_time < now; | |
163 }); | |
164 DVLOG(2) << "After clean up, cache size: " << description_map_.size(); | |
mark a. foltz
2017/04/18 18:16:26
If the cache is emptied and there are no pending r
zhaobin
2017/04/21 23:12:58
Done.
| |
165 } | |
166 | |
167 base::Time DeviceDescriptionService::GetNow() { | |
mark a. foltz
2017/04/18 18:16:26
Is this for unit testing?
zhaobin
2017/04/21 23:12:58
Yes.
| |
168 return base::Time::Now(); | |
169 } | |
170 | |
171 void DeviceDescriptionService::FetchDeviceDescription( | |
172 const DialDeviceData& device_data, | |
173 net::URLRequestContextGetter* request_context) { | |
174 DCHECK(thread_checker_.CalledOnValidThread()); | |
175 | |
176 auto device_description_fetcher = base::MakeUnique<DeviceDescriptionFetcher>( | |
177 device_data.device_description_url(), request_context, | |
178 base::BindOnce( | |
179 &DeviceDescriptionService::OnDeviceDescriptionFetchComplete, | |
180 base::Unretained(this), device_data), | |
181 base::BindOnce(&DeviceDescriptionService::OnDeviceDescriptionFetchError, | |
182 base::Unretained(this), device_data)); | |
183 | |
184 device_description_fetcher->Start(); | |
185 device_description_fetcher_map_.insert(std::make_pair( | |
186 device_data.label(), std::move(device_description_fetcher))); | |
187 } | |
188 | |
189 const DeviceDescriptionService::CacheEntry* | |
190 DeviceDescriptionService::CheckAndUpdateCache( | |
191 const DialDeviceData& device_data) { | |
192 const auto& it = description_map_.find(device_data.label()); | |
193 if (it == description_map_.end()) | |
194 return nullptr; | |
195 | |
196 // If the entry's config_id does not match, or it has expired, remove it. | |
197 if (it->second.config_id != device_data.config_id() || | |
198 GetNow() >= it->second.expire_time) { | |
199 DVLOG(2) << "Removing invalid entry " << it->first; | |
200 description_map_.erase(it); | |
201 return nullptr; | |
202 } | |
203 | |
204 // Entry is valid. | |
205 return &it->second; | |
206 } | |
207 | |
208 void DeviceDescriptionService::OnParsedDeviceDescription( | |
209 const DialDeviceData& device_data, | |
210 const GURL& app_url, | |
211 chrome::mojom::DialDeviceDescriptionPtr parsed_device_description) { | |
212 DCHECK(thread_checker_.CalledOnValidThread()); | |
213 | |
214 // Last callback for current utility process. Release |parser_| and | |
215 // SafeDialDeviceDescriptionParser object will be destroyed after this | |
216 // callback. | |
217 if (parser_ && parser_->GetPendingParsingRequests() == 0) | |
mark a. foltz
2017/04/18 18:16:26
For a given list of devices, is it possible for on
zhaobin
2017/04/21 23:12:57
Done.
| |
218 parser_ = nullptr; | |
219 | |
220 if (!parsed_device_description) { | |
221 error_cb_.Run(device_data, "Failed to parse device description xml"); | |
222 return; | |
223 } | |
224 | |
225 ParsedDialDeviceDescription description_data; | |
226 description_data.unique_id = parsed_device_description->unique_id; | |
227 description_data.friendly_name = parsed_device_description->friendly_name; | |
228 description_data.model_name = parsed_device_description->model_name; | |
229 description_data.app_url = app_url; | |
230 | |
231 auto error = ValidateParsedDeviceDescription( | |
232 device_data.device_description_url().host(), description_data); | |
233 | |
234 if (error != ErrorType::NONE) { | |
235 DLOG(WARNING) << "Device description failed to validate: " << error; | |
236 error_cb_.Run(device_data, "Failed to process fetch result"); | |
237 return; | |
238 } | |
239 | |
240 CacheEntry cached_description_data; | |
241 cached_description_data.expire_time = | |
242 GetNow() + | |
243 base::TimeDelta::FromMilliseconds(kDeviceDescriptionCacheTimeMillis); | |
mark a. foltz
2017/04/18 18:16:26
Can you convert the cache timeout constant to use
zhaobin
2017/04/21 23:12:58
Done.
| |
244 cached_description_data.config_id = device_data.config_id(); | |
245 cached_description_data.description_data = description_data; | |
246 | |
247 DVLOG(2) << "Got device description for " << device_data.label() | |
248 << "... device description was: " | |
249 << CachedDeviceDescriptionToString(cached_description_data); | |
250 | |
251 // Stick it in the cache if the description has a config (version) id. | |
252 // NOTE: We could cache descriptions without a config id and rely on the | |
mark a. foltz
2017/04/18 18:16:26
Yes we should do this, to avoid repeatedly request
zhaobin
2017/04/21 23:12:58
Done.
| |
253 // timeout to eventually update the cache. | |
254 if (description_map_.size() < kCacheLimit && | |
mark a. foltz
2017/04/18 18:16:26
If the cache size limit has been hit, you can earl
zhaobin
2017/04/21 23:12:57
Done.
| |
255 cached_description_data.config_id) { | |
256 DVLOG(2) << "Caching device description for " << device_data.label(); | |
257 description_map_.insert( | |
258 std::make_pair(device_data.label(), cached_description_data)); | |
259 } | |
260 | |
261 success_cb_.Run(device_data, description_data); | |
262 } | |
263 | |
264 void DeviceDescriptionService::OnDeviceDescriptionFetchComplete( | |
265 const DialDeviceData& device_data, | |
266 const DialDeviceDescriptionData& description_data) { | |
267 DCHECK(thread_checker_.CalledOnValidThread()); | |
268 | |
269 if (!parser_) | |
270 parser_ = new SafeDialDeviceDescriptionParser(); | |
271 | |
272 parser_->Start( | |
273 description_data.device_description, | |
274 base::Bind(&DeviceDescriptionService::OnParsedDeviceDescription, | |
275 base::Unretained(this), device_data, | |
276 description_data.app_url)); | |
277 | |
278 DVLOG(2) << "Device description: " | |
279 << ScrubDeviceDescriptionXml(description_data.device_description); | |
280 device_description_fetcher_map_.erase(device_data.label()); | |
281 } | |
282 | |
283 void DeviceDescriptionService::OnDeviceDescriptionFetchError( | |
284 const DialDeviceData& device_data, | |
285 const std::string& error_message) { | |
286 DCHECK(thread_checker_.CalledOnValidThread()); | |
287 DVLOG(2) << "OnDeviceDescriptionFetchError [label]: " << device_data.label(); | |
288 | |
289 error_cb_.Run(device_data, error_message); | |
290 device_description_fetcher_map_.erase(device_data.label()); | |
291 } | |
292 | |
293 DeviceDescriptionService::CacheEntry::CacheEntry() : config_id(-1) {} | |
294 DeviceDescriptionService::CacheEntry::CacheEntry(const CacheEntry& other) = | |
295 default; | |
296 DeviceDescriptionService::CacheEntry::~CacheEntry() = default; | |
297 | |
298 } // namespace media_router | |
OLD | NEW |