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