Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / net / base / sdch_manager.cc
1 // Copyright (c) 2012 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 "net/base/sdch_manager.h"
6
7 #include "base/base64.h"
8 #include "base/logging.h"
9 #include "base/metrics/histogram.h"
10 #include "base/strings/string_number_conversions.h"
11 #include "base/strings/string_util.h"
12 #include "crypto/sha2.h"
13 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
14 #include "net/base/sdch_observer.h"
15 #include "net/url_request/url_request_http_job.h"
16
17 namespace {
18
19 void StripTrailingDot(GURL* gurl) {
20   std::string host(gurl->host());
21
22   if (host.empty())
23     return;
24
25   if (*host.rbegin() != '.')
26     return;
27
28   host.resize(host.size() - 1);
29
30   GURL::Replacements replacements;
31   replacements.SetHostStr(host);
32   *gurl = gurl->ReplaceComponents(replacements);
33   return;
34 }
35
36 }  // namespace
37
38 namespace net {
39
40 //------------------------------------------------------------------------------
41 // static
42
43 // Adjust SDCH limits downwards for mobile.
44 #if defined(OS_ANDROID) || defined(OS_IOS)
45 // static
46 const size_t SdchManager::kMaxDictionaryCount = 1;
47 const size_t SdchManager::kMaxDictionarySize = 500 * 1000;
48 #else
49 // static
50 const size_t SdchManager::kMaxDictionaryCount = 20;
51 const size_t SdchManager::kMaxDictionarySize = 1000 * 1000;
52 #endif
53
54 // static
55 bool SdchManager::g_sdch_enabled_ = true;
56
57 // static
58 bool SdchManager::g_secure_scheme_supported_ = true;
59
60 //------------------------------------------------------------------------------
61 SdchManager::Dictionary::Dictionary(const std::string& dictionary_text,
62                                     size_t offset,
63                                     const std::string& client_hash,
64                                     const GURL& gurl,
65                                     const std::string& domain,
66                                     const std::string& path,
67                                     const base::Time& expiration,
68                                     const std::set<int>& ports)
69     : text_(dictionary_text, offset),
70       client_hash_(client_hash),
71       url_(gurl),
72       domain_(domain),
73       path_(path),
74       expiration_(expiration),
75       ports_(ports) {
76 }
77
78 SdchManager::Dictionary::~Dictionary() {
79 }
80
81 bool SdchManager::Dictionary::CanAdvertise(const GURL& target_url) {
82   /* The specific rules of when a dictionary should be advertised in an
83      Avail-Dictionary header are modeled after the rules for cookie scoping. The
84      terms "domain-match" and "pathmatch" are defined in RFC 2965 [6]. A
85      dictionary may be advertised in the Avail-Dictionaries header exactly when
86      all of the following are true:
87       1. The server's effective host name domain-matches the Domain attribute of
88          the dictionary.
89       2. If the dictionary has a Port attribute, the request port is one of the
90          ports listed in the Port attribute.
91       3. The request URI path-matches the path header of the dictionary.
92       4. The request is not an HTTPS request.
93      We can override (ignore) item (4) only when we have explicitly enabled
94      HTTPS support AND the dictionary acquisition scheme matches the target
95      url scheme.
96     */
97   if (!DomainMatch(target_url, domain_))
98     return false;
99   if (!ports_.empty() && 0 == ports_.count(target_url.EffectiveIntPort()))
100     return false;
101   if (path_.size() && !PathMatch(target_url.path(), path_))
102     return false;
103   if (!SdchManager::secure_scheme_supported() && target_url.SchemeIsSecure())
104     return false;
105   if (target_url.SchemeIsSecure() != url_.SchemeIsSecure())
106     return false;
107   if (base::Time::Now() > expiration_)
108     return false;
109   return true;
110 }
111
112 //------------------------------------------------------------------------------
113 // Security functions restricting loads and use of dictionaries.
114
115 // static
116 bool SdchManager::Dictionary::CanSet(const std::string& domain,
117                                      const std::string& path,
118                                      const std::set<int>& ports,
119                                      const GURL& dictionary_url) {
120   /*
121   A dictionary is invalid and must not be stored if any of the following are
122   true:
123     1. The dictionary has no Domain attribute.
124     2. The effective host name that derives from the referer URL host name does
125       not domain-match the Domain attribute.
126     3. The Domain attribute is a top level domain.
127     4. The referer URL host is a host domain name (not IP address) and has the
128       form HD, where D is the value of the Domain attribute, and H is a string
129       that contains one or more dots.
130     5. If the dictionary has a Port attribute and the referer URL's port was not
131       in the list.
132   */
133
134   // TODO(jar): Redirects in dictionary fetches might plausibly be problematic,
135   // and hence the conservative approach is to not allow any redirects (if there
136   // were any... then don't allow the dictionary to be set).
137
138   if (domain.empty()) {
139     SdchErrorRecovery(DICTIONARY_MISSING_DOMAIN_SPECIFIER);
140     return false;  // Domain is required.
141   }
142   if (registry_controlled_domains::GetDomainAndRegistry(
143         domain,
144         registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES).empty()) {
145     SdchErrorRecovery(DICTIONARY_SPECIFIES_TOP_LEVEL_DOMAIN);
146     return false;  // domain was a TLD.
147   }
148   if (!Dictionary::DomainMatch(dictionary_url, domain)) {
149     SdchErrorRecovery(DICTIONARY_DOMAIN_NOT_MATCHING_SOURCE_URL);
150     return false;
151   }
152
153   std::string referrer_url_host = dictionary_url.host();
154   size_t postfix_domain_index = referrer_url_host.rfind(domain);
155   // See if it is indeed a postfix, or just an internal string.
156   if (referrer_url_host.size() == postfix_domain_index + domain.size()) {
157     // It is a postfix... so check to see if there's a dot in the prefix.
158     size_t end_of_host_index = referrer_url_host.find_first_of('.');
159     if (referrer_url_host.npos != end_of_host_index  &&
160         end_of_host_index < postfix_domain_index) {
161       SdchErrorRecovery(DICTIONARY_REFERER_URL_HAS_DOT_IN_PREFIX);
162       return false;
163     }
164   }
165
166   if (!ports.empty()
167       && 0 == ports.count(dictionary_url.EffectiveIntPort())) {
168     SdchErrorRecovery(DICTIONARY_PORT_NOT_MATCHING_SOURCE_URL);
169     return false;
170   }
171   return true;
172 }
173
174 // static
175 bool SdchManager::Dictionary::CanUse(const GURL& referring_url) {
176   /*
177     1. The request URL's host name domain-matches the Domain attribute of the
178       dictionary.
179     2. If the dictionary has a Port attribute, the request port is one of the
180       ports listed in the Port attribute.
181     3. The request URL path-matches the path attribute of the dictionary.
182     4. The request is not an HTTPS request.
183     We can override (ignore) item (4) only when we have explicitly enabled
184     HTTPS support AND the dictionary acquisition scheme matches the target
185      url scheme.
186   */
187   if (!DomainMatch(referring_url, domain_)) {
188     SdchErrorRecovery(DICTIONARY_FOUND_HAS_WRONG_DOMAIN);
189     return false;
190   }
191   if (!ports_.empty()
192       && 0 == ports_.count(referring_url.EffectiveIntPort())) {
193     SdchErrorRecovery(DICTIONARY_FOUND_HAS_WRONG_PORT_LIST);
194     return false;
195   }
196   if (path_.size() && !PathMatch(referring_url.path(), path_)) {
197     SdchErrorRecovery(DICTIONARY_FOUND_HAS_WRONG_PATH);
198     return false;
199   }
200   if (!SdchManager::secure_scheme_supported() &&
201       referring_url.SchemeIsSecure()) {
202     SdchErrorRecovery(DICTIONARY_FOUND_HAS_WRONG_SCHEME);
203     return false;
204   }
205   if (referring_url.SchemeIsSecure() != url_.SchemeIsSecure()) {
206     SdchErrorRecovery(DICTIONARY_FOUND_HAS_WRONG_SCHEME);
207     return false;
208   }
209
210   // TODO(jar): Remove overly restrictive failsafe test (added per security
211   // review) when we have a need to be more general.
212   if (!referring_url.SchemeIsHTTPOrHTTPS()) {
213     SdchErrorRecovery(ATTEMPT_TO_DECODE_NON_HTTP_DATA);
214     return false;
215   }
216
217   return true;
218 }
219
220 bool SdchManager::Dictionary::PathMatch(const std::string& path,
221                                         const std::string& restriction) {
222   /*  Must be either:
223   1. P2 is equal to P1
224   2. P2 is a prefix of P1 and either the final character in P2 is "/" or the
225       character following P2 in P1 is "/".
226       */
227   if (path == restriction)
228     return true;
229   size_t prefix_length = restriction.size();
230   if (prefix_length > path.size())
231     return false;  // Can't be a prefix.
232   if (0 != path.compare(0, prefix_length, restriction))
233     return false;
234   return restriction[prefix_length - 1] == '/' || path[prefix_length] == '/';
235 }
236
237 // static
238 bool SdchManager::Dictionary::DomainMatch(const GURL& gurl,
239                                           const std::string& restriction) {
240   // TODO(jar): This is not precisely a domain match definition.
241   return gurl.DomainIs(restriction.data(), restriction.size());
242 }
243
244 //------------------------------------------------------------------------------
245 SdchManager::SdchManager() {
246   DCHECK(thread_checker_.CalledOnValidThread());
247 }
248
249 SdchManager::~SdchManager() {
250   DCHECK(thread_checker_.CalledOnValidThread());
251   while (!dictionaries_.empty()) {
252     DictionaryMap::iterator it = dictionaries_.begin();
253     dictionaries_.erase(it->first);
254   }
255 }
256
257 void SdchManager::ClearData() {
258   blacklisted_domains_.clear();
259   allow_latency_experiment_.clear();
260
261   // Note that this may result in not having dictionaries we've advertised
262   // for incoming responses.  The window is relatively small (as ClearData()
263   // is not expected to be called frequently), so we rely on meta-refresh
264   // to handle this case.
265   dictionaries_.clear();
266
267   FOR_EACH_OBSERVER(SdchObserver, observers_, OnClearDictionaries(this));
268 }
269
270 // static
271 void SdchManager::SdchErrorRecovery(ProblemCodes problem) {
272   UMA_HISTOGRAM_ENUMERATION("Sdch3.ProblemCodes_4", problem, MAX_PROBLEM_CODE);
273 }
274
275 // static
276 void SdchManager::EnableSdchSupport(bool enabled) {
277   g_sdch_enabled_ = enabled;
278 }
279
280 // static
281 void SdchManager::EnableSecureSchemeSupport(bool enabled) {
282   g_secure_scheme_supported_ = enabled;
283 }
284
285 void SdchManager::BlacklistDomain(const GURL& url,
286                                   ProblemCodes blacklist_reason) {
287   SetAllowLatencyExperiment(url, false);
288
289   BlacklistInfo* blacklist_info =
290       &blacklisted_domains_[base::StringToLowerASCII(url.host())];
291
292   if (blacklist_info->count > 0)
293     return;  // Domain is already blacklisted.
294
295   if (blacklist_info->exponential_count > (INT_MAX - 1) / 2) {
296     blacklist_info->exponential_count = INT_MAX;
297   } else {
298     blacklist_info->exponential_count =
299         blacklist_info->exponential_count * 2 + 1;
300   }
301
302   blacklist_info->count = blacklist_info->exponential_count;
303   blacklist_info->reason = blacklist_reason;
304 }
305
306 void SdchManager::BlacklistDomainForever(const GURL& url,
307                                          ProblemCodes blacklist_reason) {
308   SetAllowLatencyExperiment(url, false);
309
310   BlacklistInfo* blacklist_info =
311       &blacklisted_domains_[base::StringToLowerASCII(url.host())];
312   blacklist_info->count = INT_MAX;
313   blacklist_info->exponential_count = INT_MAX;
314   blacklist_info->reason = blacklist_reason;
315 }
316
317 void SdchManager::ClearBlacklistings() {
318   blacklisted_domains_.clear();
319 }
320
321 void SdchManager::ClearDomainBlacklisting(const std::string& domain) {
322   BlacklistInfo* blacklist_info = &blacklisted_domains_[
323       base::StringToLowerASCII(domain)];
324   blacklist_info->count = 0;
325   blacklist_info->reason = MIN_PROBLEM_CODE;
326 }
327
328 int SdchManager::BlackListDomainCount(const std::string& domain) {
329   std::string domain_lower(base::StringToLowerASCII(domain));
330
331   if (blacklisted_domains_.end() == blacklisted_domains_.find(domain_lower))
332     return 0;
333   return blacklisted_domains_[domain_lower].count;
334 }
335
336 int SdchManager::BlacklistDomainExponential(const std::string& domain) {
337   std::string domain_lower(base::StringToLowerASCII(domain));
338
339   if (blacklisted_domains_.end() == blacklisted_domains_.find(domain_lower))
340     return 0;
341   return blacklisted_domains_[domain_lower].exponential_count;
342 }
343
344 bool SdchManager::IsInSupportedDomain(const GURL& url) {
345   DCHECK(thread_checker_.CalledOnValidThread());
346   if (!g_sdch_enabled_ )
347     return false;
348
349   if (!secure_scheme_supported() && url.SchemeIsSecure())
350     return false;
351
352   if (blacklisted_domains_.empty())
353     return true;
354
355   DomainBlacklistInfo::iterator it =
356       blacklisted_domains_.find(base::StringToLowerASCII(url.host()));
357   if (blacklisted_domains_.end() == it || it->second.count == 0)
358     return true;
359
360   UMA_HISTOGRAM_ENUMERATION("Sdch3.BlacklistReason", it->second.reason,
361                             MAX_PROBLEM_CODE);
362   SdchErrorRecovery(DOMAIN_BLACKLIST_INCLUDES_TARGET);
363
364   int count = it->second.count - 1;
365   if (count > 0) {
366     it->second.count = count;
367   } else {
368     it->second.count = 0;
369     it->second.reason = MIN_PROBLEM_CODE;
370   }
371
372   return false;
373 }
374
375 void SdchManager::OnGetDictionary(const GURL& request_url,
376                                   const GURL& dictionary_url) {
377   if (!CanFetchDictionary(request_url, dictionary_url))
378     return;
379
380   FOR_EACH_OBSERVER(SdchObserver,
381                     observers_,
382                     OnGetDictionary(this, request_url, dictionary_url));
383 }
384
385 bool SdchManager::CanFetchDictionary(const GURL& referring_url,
386                                      const GURL& dictionary_url) const {
387   DCHECK(thread_checker_.CalledOnValidThread());
388   /* The user agent may retrieve a dictionary from the dictionary URL if all of
389      the following are true:
390        1 The dictionary URL host name matches the referrer URL host name and
391            scheme.
392        2 The dictionary URL host name domain matches the parent domain of the
393            referrer URL host name
394        3 The parent domain of the referrer URL host name is not a top level
395            domain
396    */
397   // Item (1) above implies item (2).  Spec should be updated.
398   // I take "host name match" to be "is identical to"
399   if (referring_url.host() != dictionary_url.host() ||
400       referring_url.scheme() != dictionary_url.scheme()) {
401     SdchErrorRecovery(DICTIONARY_LOAD_ATTEMPT_FROM_DIFFERENT_HOST);
402     return false;
403   }
404   if (!secure_scheme_supported() && referring_url.SchemeIsSecure()) {
405     SdchErrorRecovery(DICTIONARY_SELECTED_FOR_SSL);
406     return false;
407   }
408
409   // TODO(jar): Remove this failsafe conservative hack which is more restrictive
410   // than current SDCH spec when needed, and justified by security audit.
411   if (!referring_url.SchemeIsHTTPOrHTTPS()) {
412     SdchErrorRecovery(DICTIONARY_SELECTED_FROM_NON_HTTP);
413     return false;
414   }
415
416   return true;
417 }
418
419 void SdchManager::GetVcdiffDictionary(
420     const std::string& server_hash,
421     const GURL& referring_url,
422     scoped_refptr<Dictionary>* dictionary) {
423   DCHECK(thread_checker_.CalledOnValidThread());
424   *dictionary = NULL;
425   DictionaryMap::iterator it = dictionaries_.find(server_hash);
426   if (it == dictionaries_.end()) {
427     return;
428   }
429   scoped_refptr<Dictionary> matching_dictionary = it->second;
430   if (!IsInSupportedDomain(referring_url))
431     return;
432   if (!matching_dictionary->CanUse(referring_url))
433     return;
434   *dictionary = matching_dictionary;
435 }
436
437 // TODO(jar): If we have evictions from the dictionaries_, then we need to
438 // change this interface to return a list of reference counted Dictionary
439 // instances that can be used if/when a server specifies one.
440 void SdchManager::GetAvailDictionaryList(const GURL& target_url,
441                                          std::string* list) {
442   DCHECK(thread_checker_.CalledOnValidThread());
443   int count = 0;
444   for (DictionaryMap::iterator it = dictionaries_.begin();
445        it != dictionaries_.end(); ++it) {
446     if (!IsInSupportedDomain(target_url))
447       continue;
448     if (!it->second->CanAdvertise(target_url))
449       continue;
450     ++count;
451     if (!list->empty())
452       list->append(",");
453     list->append(it->second->client_hash());
454   }
455   // Watch to see if we have corrupt or numerous dictionaries.
456   if (count > 0)
457     UMA_HISTOGRAM_COUNTS("Sdch3.Advertisement_Count", count);
458 }
459
460 // static
461 void SdchManager::GenerateHash(const std::string& dictionary_text,
462     std::string* client_hash, std::string* server_hash) {
463   char binary_hash[32];
464   crypto::SHA256HashString(dictionary_text, binary_hash, sizeof(binary_hash));
465
466   std::string first_48_bits(&binary_hash[0], 6);
467   std::string second_48_bits(&binary_hash[6], 6);
468   UrlSafeBase64Encode(first_48_bits, client_hash);
469   UrlSafeBase64Encode(second_48_bits, server_hash);
470
471   DCHECK_EQ(server_hash->length(), 8u);
472   DCHECK_EQ(client_hash->length(), 8u);
473 }
474
475 //------------------------------------------------------------------------------
476 // Methods for supporting latency experiments.
477
478 bool SdchManager::AllowLatencyExperiment(const GURL& url) const {
479   DCHECK(thread_checker_.CalledOnValidThread());
480   return allow_latency_experiment_.end() !=
481       allow_latency_experiment_.find(url.host());
482 }
483
484 void SdchManager::SetAllowLatencyExperiment(const GURL& url, bool enable) {
485   DCHECK(thread_checker_.CalledOnValidThread());
486   if (enable) {
487     allow_latency_experiment_.insert(url.host());
488     return;
489   }
490   ExperimentSet::iterator it = allow_latency_experiment_.find(url.host());
491   if (allow_latency_experiment_.end() == it)
492     return;  // It was already erased, or never allowed.
493   SdchErrorRecovery(LATENCY_TEST_DISALLOWED);
494   allow_latency_experiment_.erase(it);
495 }
496
497 void SdchManager::AddObserver(SdchObserver* observer) {
498   observers_.AddObserver(observer);
499 }
500
501 void SdchManager::RemoveObserver(SdchObserver* observer) {
502   observers_.RemoveObserver(observer);
503 }
504
505 void SdchManager::AddSdchDictionary(const std::string& dictionary_text,
506     const GURL& dictionary_url) {
507   DCHECK(thread_checker_.CalledOnValidThread());
508   std::string client_hash;
509   std::string server_hash;
510   GenerateHash(dictionary_text, &client_hash, &server_hash);
511   if (dictionaries_.find(server_hash) != dictionaries_.end()) {
512     SdchErrorRecovery(DICTIONARY_ALREADY_LOADED);
513     return;                             // Already loaded.
514   }
515
516   std::string domain, path;
517   std::set<int> ports;
518   base::Time expiration(base::Time::Now() + base::TimeDelta::FromDays(30));
519
520   if (dictionary_text.empty()) {
521     SdchErrorRecovery(DICTIONARY_HAS_NO_TEXT);
522     return;                             // Missing header.
523   }
524
525   size_t header_end = dictionary_text.find("\n\n");
526   if (std::string::npos == header_end) {
527     SdchErrorRecovery(DICTIONARY_HAS_NO_HEADER);
528     return;                             // Missing header.
529   }
530   size_t line_start = 0;  // Start of line being parsed.
531   while (1) {
532     size_t line_end = dictionary_text.find('\n', line_start);
533     DCHECK(std::string::npos != line_end);
534     DCHECK_LE(line_end, header_end);
535
536     size_t colon_index = dictionary_text.find(':', line_start);
537     if (std::string::npos == colon_index) {
538       SdchErrorRecovery(DICTIONARY_HEADER_LINE_MISSING_COLON);
539       return;                         // Illegal line missing a colon.
540     }
541
542     if (colon_index > line_end)
543       break;
544
545     size_t value_start = dictionary_text.find_first_not_of(" \t",
546                                                            colon_index + 1);
547     if (std::string::npos != value_start) {
548       if (value_start >= line_end)
549         break;
550       std::string name(dictionary_text, line_start, colon_index - line_start);
551       std::string value(dictionary_text, value_start, line_end - value_start);
552       name = base::StringToLowerASCII(name);
553       if (name == "domain") {
554         domain = value;
555       } else if (name == "path") {
556         path = value;
557       } else if (name == "format-version") {
558         if (value != "1.0")
559           return;
560       } else if (name == "max-age") {
561         int64 seconds;
562         base::StringToInt64(value, &seconds);
563         expiration = base::Time::Now() + base::TimeDelta::FromSeconds(seconds);
564       } else if (name == "port") {
565         int port;
566         base::StringToInt(value, &port);
567         if (port >= 0)
568           ports.insert(port);
569       }
570     }
571
572     if (line_end >= header_end)
573       break;
574     line_start = line_end + 1;
575   }
576
577   // Narrow fix for http://crbug.com/389451.
578   GURL dictionary_url_normalized(dictionary_url);
579   StripTrailingDot(&dictionary_url_normalized);
580
581   if (!IsInSupportedDomain(dictionary_url_normalized))
582     return;
583
584   if (!Dictionary::CanSet(domain, path, ports, dictionary_url_normalized))
585     return;
586
587   // TODO(jar): Remove these hacks to preclude a DOS attack involving piles of
588   // useless dictionaries.  We should probably have a cache eviction plan,
589   // instead of just blocking additions.  For now, with the spec in flux, it
590   // is probably not worth doing eviction handling.
591   if (kMaxDictionarySize < dictionary_text.size()) {
592     SdchErrorRecovery(DICTIONARY_IS_TOO_LARGE);
593     return;
594   }
595   if (kMaxDictionaryCount <= dictionaries_.size()) {
596     SdchErrorRecovery(DICTIONARY_COUNT_EXCEEDED);
597     return;
598   }
599
600   UMA_HISTOGRAM_COUNTS("Sdch3.Dictionary size loaded", dictionary_text.size());
601   DVLOG(1) << "Loaded dictionary with client hash " << client_hash
602            << " and server hash " << server_hash;
603   Dictionary* dictionary =
604       new Dictionary(dictionary_text, header_end + 2, client_hash,
605                      dictionary_url_normalized, domain,
606                      path, expiration, ports);
607   dictionaries_[server_hash] = dictionary;
608   return;
609 }
610
611 // static
612 void SdchManager::UrlSafeBase64Encode(const std::string& input,
613                                       std::string* output) {
614   // Since this is only done during a dictionary load, and hashes are only 8
615   // characters, we just do the simple fixup, rather than rewriting the encoder.
616   base::Base64Encode(input, output);
617   std::replace(output->begin(), output->end(), '+', '-');
618   std::replace(output->begin(), output->end(), '/', '_');
619 }
620
621 }  // namespace net