OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "net/base/sdch_manager.h" | 5 #include "net/base/sdch_manager.h" |
6 | 6 |
7 #include "base/base64.h" | 7 #include "base/base64.h" |
8 #include "base/logging.h" | 8 #include "base/logging.h" |
9 #include "base/metrics/histogram.h" | 9 #include "base/metrics/histogram.h" |
10 #include "base/strings/string_number_conversions.h" | 10 #include "base/strings/string_number_conversions.h" |
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
71 url_(gurl), | 71 url_(gurl), |
72 domain_(domain), | 72 domain_(domain), |
73 path_(path), | 73 path_(path), |
74 expiration_(expiration), | 74 expiration_(expiration), |
75 ports_(ports) { | 75 ports_(ports) { |
76 } | 76 } |
77 | 77 |
78 SdchManager::Dictionary::~Dictionary() { | 78 SdchManager::Dictionary::~Dictionary() { |
79 } | 79 } |
80 | 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 //------------------------------------------------------------------------------ | 81 //------------------------------------------------------------------------------ |
113 // Security functions restricting loads and use of dictionaries. | 82 // Security functions restricting loads and use of dictionaries. |
114 | 83 |
115 // static | 84 // static |
116 bool SdchManager::Dictionary::CanSet(const std::string& domain, | 85 bool SdchManager::Dictionary::CanSet(const std::string& domain, |
117 const std::string& path, | 86 const std::string& path, |
118 const std::set<int>& ports, | 87 const std::set<int>& ports, |
119 const GURL& dictionary_url) { | 88 const GURL& dictionary_url) { |
120 /* | 89 /* |
121 A dictionary is invalid and must not be stored if any of the following are | 90 A dictionary is invalid and must not be stored if any of the following are |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
164 } | 133 } |
165 | 134 |
166 if (!ports.empty() | 135 if (!ports.empty() |
167 && 0 == ports.count(dictionary_url.EffectiveIntPort())) { | 136 && 0 == ports.count(dictionary_url.EffectiveIntPort())) { |
168 SdchErrorRecovery(DICTIONARY_PORT_NOT_MATCHING_SOURCE_URL); | 137 SdchErrorRecovery(DICTIONARY_PORT_NOT_MATCHING_SOURCE_URL); |
169 return false; | 138 return false; |
170 } | 139 } |
171 return true; | 140 return true; |
172 } | 141 } |
173 | 142 |
174 // static | 143 SdchManager::ProblemCodes |
175 bool SdchManager::Dictionary::CanUse(const GURL& referring_url) { | 144 SdchManager::Dictionary::CanUse(const GURL& target_url) { |
176 /* | 145 /* |
177 1. The request URL's host name domain-matches the Domain attribute of the | 146 1. The request URL's host name domain-matches the Domain attribute of the |
178 dictionary. | 147 dictionary. |
179 2. If the dictionary has a Port attribute, the request port is one of the | 148 2. If the dictionary has a Port attribute, the request port is one of the |
180 ports listed in the Port attribute. | 149 ports listed in the Port attribute. |
181 3. The request URL path-matches the path attribute of the dictionary. | 150 3. The request URL path-matches the path attribute of the dictionary. |
182 4. The request is not an HTTPS request. | 151 4. The request is not an HTTPS request. |
183 We can override (ignore) item (4) only when we have explicitly enabled | 152 We can override (ignore) item (4) only when we have explicitly enabled |
184 HTTPS support AND the dictionary acquisition scheme matches the target | 153 HTTPS support AND the dictionary acquisition scheme matches the target |
185 url scheme. | 154 url scheme. |
186 */ | 155 */ |
187 if (!DomainMatch(referring_url, domain_)) { | 156 if (!DomainMatch(target_url, domain_)) |
188 SdchErrorRecovery(DICTIONARY_FOUND_HAS_WRONG_DOMAIN); | 157 return DICTIONARY_FOUND_HAS_WRONG_DOMAIN; |
189 return false; | 158 if (!ports_.empty() && 0 == ports_.count(target_url.EffectiveIntPort())) |
190 } | 159 return DICTIONARY_FOUND_HAS_WRONG_PORT_LIST; |
191 if (!ports_.empty() | 160 if (path_.size() && !PathMatch(target_url.path(), path_)) |
192 && 0 == ports_.count(referring_url.EffectiveIntPort())) { | 161 return DICTIONARY_FOUND_HAS_WRONG_PATH; |
193 SdchErrorRecovery(DICTIONARY_FOUND_HAS_WRONG_PORT_LIST); | 162 if (!SdchManager::secure_scheme_supported() && target_url.SchemeIsSecure()) |
194 return false; | 163 return DICTIONARY_FOUND_HAS_WRONG_SCHEME; |
195 } | 164 if (target_url.SchemeIsSecure() != url_.SchemeIsSecure()) |
196 if (path_.size() && !PathMatch(referring_url.path(), path_)) { | 165 return DICTIONARY_FOUND_HAS_WRONG_SCHEME; |
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 | 166 |
210 // TODO(jar): Remove overly restrictive failsafe test (added per security | 167 // TODO(jar): Remove overly restrictive failsafe test (added per security |
211 // review) when we have a need to be more general. | 168 // review) when we have a need to be more general. |
212 if (!referring_url.SchemeIsHTTPOrHTTPS()) { | 169 if (!target_url.SchemeIsHTTPOrHTTPS()) |
213 SdchErrorRecovery(ATTEMPT_TO_DECODE_NON_HTTP_DATA); | 170 return ATTEMPT_TO_DECODE_NON_HTTP_DATA; |
214 return false; | |
215 } | |
216 | 171 |
217 return true; | 172 return OK; |
218 } | 173 } |
219 | 174 |
220 bool SdchManager::Dictionary::PathMatch(const std::string& path, | 175 bool SdchManager::Dictionary::PathMatch(const std::string& path, |
221 const std::string& restriction) { | 176 const std::string& restriction) { |
222 /* Must be either: | 177 /* Must be either: |
223 1. P2 is equal to P1 | 178 1. P2 is equal to P1 |
224 2. P2 is a prefix of P1 and either the final character in P2 is "/" or the | 179 2. P2 is a prefix of P1 and either the final character in P2 is "/" or the |
225 character following P2 in P1 is "/". | 180 character following P2 in P1 is "/". |
226 */ | 181 */ |
227 if (path == restriction) | 182 if (path == restriction) |
228 return true; | 183 return true; |
229 size_t prefix_length = restriction.size(); | 184 size_t prefix_length = restriction.size(); |
230 if (prefix_length > path.size()) | 185 if (prefix_length > path.size()) |
231 return false; // Can't be a prefix. | 186 return false; // Can't be a prefix. |
232 if (0 != path.compare(0, prefix_length, restriction)) | 187 if (0 != path.compare(0, prefix_length, restriction)) |
233 return false; | 188 return false; |
234 return restriction[prefix_length - 1] == '/' || path[prefix_length] == '/'; | 189 return restriction[prefix_length - 1] == '/' || path[prefix_length] == '/'; |
235 } | 190 } |
236 | 191 |
237 // static | 192 // static |
238 bool SdchManager::Dictionary::DomainMatch(const GURL& gurl, | 193 bool SdchManager::Dictionary::DomainMatch(const GURL& gurl, |
239 const std::string& restriction) { | 194 const std::string& restriction) { |
240 // TODO(jar): This is not precisely a domain match definition. | 195 // TODO(jar): This is not precisely a domain match definition. |
241 return gurl.DomainIs(restriction.data(), restriction.size()); | 196 return gurl.DomainIs(restriction.data(), restriction.size()); |
242 } | 197 } |
243 | 198 |
| 199 bool SdchManager::Dictionary::Expired() const { |
| 200 return base::Time::Now() > expiration_; |
| 201 } |
| 202 |
| 203 //------------------------------------------------------------------------------ |
| 204 SdchManager::DictionaryWrapper::DictionaryWrapper( |
| 205 scoped_ptr<SdchManager::Dictionary> dictionary) |
| 206 : dictionary_(dictionary.Pass()) {} |
| 207 |
| 208 SdchManager::DictionaryWrapper::~DictionaryWrapper() {} |
| 209 |
| 210 //------------------------------------------------------------------------------ |
| 211 SdchManager::DictionarySet::DictionarySet() {} |
| 212 |
| 213 SdchManager::DictionarySet::~DictionarySet() {} |
| 214 |
| 215 void SdchManager::DictionarySet::GetDictionaryClientHashList( |
| 216 std::string* client_hashes) const { |
| 217 for (auto it = dictionaries_.begin(); it != dictionaries_.end(); ++it) { |
| 218 if (it != dictionaries_.begin()) |
| 219 client_hashes->append(","); |
| 220 |
| 221 client_hashes->append(it->second->dictionary()->client_hash()); |
| 222 } |
| 223 } |
| 224 |
| 225 const SdchManager::Dictionary* SdchManager::DictionarySet::Dictionary( |
| 226 const std::string& hash) const { |
| 227 auto it = dictionaries_.find(hash); |
| 228 if (it == dictionaries_.end()) |
| 229 return NULL; |
| 230 |
| 231 return it->second->dictionary(); |
| 232 } |
| 233 |
| 234 bool SdchManager::DictionarySet::Empty() const { |
| 235 return dictionaries_.empty(); |
| 236 } |
| 237 |
| 238 void SdchManager::DictionarySet::AddDictionary( |
| 239 const std::string& server_hash, |
| 240 scoped_refptr<SdchManager::DictionaryWrapper> dictionary) { |
| 241 DCHECK(dictionaries_.end() == dictionaries_.find(server_hash)); |
| 242 |
| 243 dictionaries_[server_hash] = dictionary; |
| 244 } |
| 245 |
244 //------------------------------------------------------------------------------ | 246 //------------------------------------------------------------------------------ |
245 SdchManager::SdchManager() { | 247 SdchManager::SdchManager() { |
246 DCHECK(thread_checker_.CalledOnValidThread()); | 248 DCHECK(thread_checker_.CalledOnValidThread()); |
247 } | 249 } |
248 | 250 |
249 SdchManager::~SdchManager() { | 251 SdchManager::~SdchManager() { |
250 DCHECK(thread_checker_.CalledOnValidThread()); | 252 DCHECK(thread_checker_.CalledOnValidThread()); |
251 while (!dictionaries_.empty()) { | 253 while (!dictionaries_.empty()) { |
252 DictionaryMap::iterator it = dictionaries_.begin(); | 254 auto it = dictionaries_.begin(); |
253 dictionaries_.erase(it->first); | 255 dictionaries_.erase(it->first); |
254 } | 256 } |
255 } | 257 } |
256 | 258 |
257 void SdchManager::ClearData() { | 259 void SdchManager::ClearData() { |
258 blacklisted_domains_.clear(); | 260 blacklisted_domains_.clear(); |
259 allow_latency_experiment_.clear(); | 261 allow_latency_experiment_.clear(); |
260 | 262 |
261 // Note that this may result in not having dictionaries we've advertised | 263 // Note that this may result in not having dictionaries we've advertised |
262 // for incoming responses. The window is relatively small (as ClearData() | 264 // for incoming responses. The window is relatively small (as ClearData() |
(...skipping 52 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
315 } | 317 } |
316 | 318 |
317 void SdchManager::ClearBlacklistings() { | 319 void SdchManager::ClearBlacklistings() { |
318 blacklisted_domains_.clear(); | 320 blacklisted_domains_.clear(); |
319 } | 321 } |
320 | 322 |
321 void SdchManager::ClearDomainBlacklisting(const std::string& domain) { | 323 void SdchManager::ClearDomainBlacklisting(const std::string& domain) { |
322 BlacklistInfo* blacklist_info = &blacklisted_domains_[ | 324 BlacklistInfo* blacklist_info = &blacklisted_domains_[ |
323 base::StringToLowerASCII(domain)]; | 325 base::StringToLowerASCII(domain)]; |
324 blacklist_info->count = 0; | 326 blacklist_info->count = 0; |
325 blacklist_info->reason = MIN_PROBLEM_CODE; | 327 blacklist_info->reason = OK; |
326 } | 328 } |
327 | 329 |
328 int SdchManager::BlackListDomainCount(const std::string& domain) { | 330 int SdchManager::BlackListDomainCount(const std::string& domain) { |
329 std::string domain_lower(base::StringToLowerASCII(domain)); | 331 std::string domain_lower(base::StringToLowerASCII(domain)); |
330 | 332 |
331 if (blacklisted_domains_.end() == blacklisted_domains_.find(domain_lower)) | 333 if (blacklisted_domains_.end() == blacklisted_domains_.find(domain_lower)) |
332 return 0; | 334 return 0; |
333 return blacklisted_domains_[domain_lower].count; | 335 return blacklisted_domains_[domain_lower].count; |
334 } | 336 } |
335 | 337 |
(...skipping 23 matching lines...) Expand all Loading... |
359 | 361 |
360 UMA_HISTOGRAM_ENUMERATION("Sdch3.BlacklistReason", it->second.reason, | 362 UMA_HISTOGRAM_ENUMERATION("Sdch3.BlacklistReason", it->second.reason, |
361 MAX_PROBLEM_CODE); | 363 MAX_PROBLEM_CODE); |
362 SdchErrorRecovery(DOMAIN_BLACKLIST_INCLUDES_TARGET); | 364 SdchErrorRecovery(DOMAIN_BLACKLIST_INCLUDES_TARGET); |
363 | 365 |
364 int count = it->second.count - 1; | 366 int count = it->second.count - 1; |
365 if (count > 0) { | 367 if (count > 0) { |
366 it->second.count = count; | 368 it->second.count = count; |
367 } else { | 369 } else { |
368 it->second.count = 0; | 370 it->second.count = 0; |
369 it->second.reason = MIN_PROBLEM_CODE; | 371 it->second.reason = OK; |
370 } | 372 } |
371 | 373 |
372 return false; | 374 return false; |
373 } | 375 } |
374 | 376 |
375 void SdchManager::OnGetDictionary(const GURL& request_url, | 377 void SdchManager::OnGetDictionary(const GURL& request_url, |
376 const GURL& dictionary_url) { | 378 const GURL& dictionary_url) { |
377 if (!CanFetchDictionary(request_url, dictionary_url)) | 379 if (!CanFetchDictionary(request_url, dictionary_url)) |
378 return; | 380 return; |
379 | 381 |
(...skipping 29 matching lines...) Expand all Loading... |
409 // TODO(jar): Remove this failsafe conservative hack which is more restrictive | 411 // TODO(jar): Remove this failsafe conservative hack which is more restrictive |
410 // than current SDCH spec when needed, and justified by security audit. | 412 // than current SDCH spec when needed, and justified by security audit. |
411 if (!referring_url.SchemeIsHTTPOrHTTPS()) { | 413 if (!referring_url.SchemeIsHTTPOrHTTPS()) { |
412 SdchErrorRecovery(DICTIONARY_SELECTED_FROM_NON_HTTP); | 414 SdchErrorRecovery(DICTIONARY_SELECTED_FROM_NON_HTTP); |
413 return false; | 415 return false; |
414 } | 416 } |
415 | 417 |
416 return true; | 418 return true; |
417 } | 419 } |
418 | 420 |
419 void SdchManager::GetVcdiffDictionary( | 421 scoped_ptr<SdchManager::DictionarySet> |
420 const std::string& server_hash, | 422 SdchManager::GetDictionarySet(const GURL& target_url) { |
421 const GURL& referring_url, | 423 if (!IsInSupportedDomain(target_url)) |
422 scoped_refptr<Dictionary>* dictionary) { | 424 return NULL; |
423 DCHECK(thread_checker_.CalledOnValidThread()); | 425 |
424 *dictionary = NULL; | 426 int count = 0; |
425 DictionaryMap::iterator it = dictionaries_.find(server_hash); | 427 scoped_ptr<SdchManager::DictionarySet> result(new DictionarySet); |
426 if (it == dictionaries_.end()) { | 428 for (auto it = dictionaries_.begin(); |
427 return; | 429 it != dictionaries_.end(); ++it) { |
| 430 if (it->second->dictionary()->CanUse(target_url) != OK) |
| 431 continue; |
| 432 if (it->second->dictionary()->Expired()) |
| 433 continue; |
| 434 ++count; |
| 435 result->AddDictionary(it->first, it->second); |
428 } | 436 } |
429 scoped_refptr<Dictionary> matching_dictionary = it->second; | 437 |
430 if (!IsInSupportedDomain(referring_url)) | 438 if (count == 0) |
431 return; | 439 return NULL; |
432 if (!matching_dictionary->CanUse(referring_url)) | 440 |
433 return; | 441 UMA_HISTOGRAM_COUNTS("Sdch3.Advertisement_Count", count); |
434 *dictionary = matching_dictionary; | 442 |
| 443 return result.Pass(); |
435 } | 444 } |
436 | 445 |
437 // TODO(jar): If we have evictions from the dictionaries_, then we need to | 446 scoped_ptr<SdchManager::DictionarySet> |
438 // change this interface to return a list of reference counted Dictionary | 447 SdchManager::GetDictionarySetByHash( |
439 // instances that can be used if/when a server specifies one. | 448 const GURL& target_url, const std::string& server_hash) { |
440 void SdchManager::GetAvailDictionaryList(const GURL& target_url, | 449 scoped_ptr<SdchManager::DictionarySet> result; |
441 std::string* list) { | 450 |
442 DCHECK(thread_checker_.CalledOnValidThread()); | 451 auto it = dictionaries_.find(server_hash); |
443 int count = 0; | 452 if (it == dictionaries_.end()) |
444 for (DictionaryMap::iterator it = dictionaries_.begin(); | 453 return result; |
445 it != dictionaries_.end(); ++it) { | 454 |
446 if (!IsInSupportedDomain(target_url)) | 455 ProblemCodes ret = it->second->dictionary()->CanUse(target_url); |
447 continue; | 456 if (ret != OK) { |
448 if (!it->second->CanAdvertise(target_url)) | 457 SdchErrorRecovery(ret); |
449 continue; | 458 return result; |
450 ++count; | |
451 if (!list->empty()) | |
452 list->append(","); | |
453 list->append(it->second->client_hash()); | |
454 } | 459 } |
455 // Watch to see if we have corrupt or numerous dictionaries. | 460 |
456 if (count > 0) | 461 result.reset(new DictionarySet); |
457 UMA_HISTOGRAM_COUNTS("Sdch3.Advertisement_Count", count); | 462 result->AddDictionary(it->first, it->second); |
| 463 return result; |
458 } | 464 } |
459 | 465 |
460 // static | 466 // static |
461 void SdchManager::GenerateHash(const std::string& dictionary_text, | 467 void SdchManager::GenerateHash(const std::string& dictionary_text, |
462 std::string* client_hash, std::string* server_hash) { | 468 std::string* client_hash, std::string* server_hash) { |
463 char binary_hash[32]; | 469 char binary_hash[32]; |
464 crypto::SHA256HashString(dictionary_text, binary_hash, sizeof(binary_hash)); | 470 crypto::SHA256HashString(dictionary_text, binary_hash, sizeof(binary_hash)); |
465 | 471 |
466 std::string first_48_bits(&binary_hash[0], 6); | 472 std::string first_48_bits(&binary_hash[0], 6); |
467 std::string second_48_bits(&binary_hash[6], 6); | 473 std::string second_48_bits(&binary_hash[6], 6); |
(...skipping 125 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
593 return; | 599 return; |
594 } | 600 } |
595 if (kMaxDictionaryCount <= dictionaries_.size()) { | 601 if (kMaxDictionaryCount <= dictionaries_.size()) { |
596 SdchErrorRecovery(DICTIONARY_COUNT_EXCEEDED); | 602 SdchErrorRecovery(DICTIONARY_COUNT_EXCEEDED); |
597 return; | 603 return; |
598 } | 604 } |
599 | 605 |
600 UMA_HISTOGRAM_COUNTS("Sdch3.Dictionary size loaded", dictionary_text.size()); | 606 UMA_HISTOGRAM_COUNTS("Sdch3.Dictionary size loaded", dictionary_text.size()); |
601 DVLOG(1) << "Loaded dictionary with client hash " << client_hash | 607 DVLOG(1) << "Loaded dictionary with client hash " << client_hash |
602 << " and server hash " << server_hash; | 608 << " and server hash " << server_hash; |
603 Dictionary* dictionary = | 609 scoped_ptr<Dictionary> dictionary( |
604 new Dictionary(dictionary_text, header_end + 2, client_hash, | 610 new Dictionary(dictionary_text, header_end + 2, client_hash, |
605 dictionary_url_normalized, domain, | 611 dictionary_url_normalized, domain, |
606 path, expiration, ports); | 612 path, expiration, ports)); |
607 dictionaries_[server_hash] = dictionary; | 613 dictionaries_[server_hash] = new DictionaryWrapper(dictionary.Pass()); |
608 return; | 614 return; |
609 } | 615 } |
610 | 616 |
611 // static | 617 // static |
| 618 scoped_ptr<SdchManager::DictionarySet> |
| 619 SdchManager::CreateNullDictionarySetForTesting() { |
| 620 return scoped_ptr<DictionarySet>(new DictionarySet).Pass(); |
| 621 } |
| 622 |
| 623 // static |
612 void SdchManager::UrlSafeBase64Encode(const std::string& input, | 624 void SdchManager::UrlSafeBase64Encode(const std::string& input, |
613 std::string* output) { | 625 std::string* output) { |
614 // Since this is only done during a dictionary load, and hashes are only 8 | 626 // 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. | 627 // characters, we just do the simple fixup, rather than rewriting the encoder. |
616 base::Base64Encode(input, output); | 628 base::Base64Encode(input, output); |
617 std::replace(output->begin(), output->end(), '+', '-'); | 629 std::replace(output->begin(), output->end(), '+', '-'); |
618 std::replace(output->begin(), output->end(), '/', '_'); | 630 std::replace(output->begin(), output->end(), '/', '_'); |
619 } | 631 } |
620 | 632 |
621 } // namespace net | 633 } // namespace net |
OLD | NEW |