Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 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 // OpenSSL binding for SSLClientSocket. The class layout and general principle | 5 // OpenSSL binding for SSLClientSocket. The class layout and general principle |
| 6 // of operation is derived from SSLClientSocketNSS. | 6 // of operation is derived from SSLClientSocketNSS. |
| 7 | 7 |
| 8 #include "net/socket/ssl_client_socket_openssl.h" | 8 #include "net/socket/ssl_client_socket_openssl.h" |
| 9 | 9 |
| 10 #include <openssl/ssl.h> | 10 #include <openssl/ssl.h> |
| (...skipping 182 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 193 | 193 |
| 194 // We do certificate verification after handshake, so we disable the default | 194 // We do certificate verification after handshake, so we disable the default |
| 195 // by registering a no-op verify function. | 195 // by registering a no-op verify function. |
| 196 int NoOpVerifyCallback(X509_STORE_CTX*, void *) { | 196 int NoOpVerifyCallback(X509_STORE_CTX*, void *) { |
| 197 DVLOG(3) << "skipping cert verify"; | 197 DVLOG(3) << "skipping cert verify"; |
| 198 return 1; | 198 return 1; |
| 199 } | 199 } |
| 200 | 200 |
| 201 // OpenSSL manages a cache of SSL_SESSION, this class provides the application | 201 // OpenSSL manages a cache of SSL_SESSION, this class provides the application |
| 202 // side policy for that cache about session re-use: we retain one session per | 202 // side policy for that cache about session re-use: we retain one session per |
| 203 // unique HostPortPair. | 203 // unique HostPortPair. |
|
joth
2011/12/09 23:28:37
per unique HostPortPair per shard.
agl
2011/12/12 22:18:20
Done.
| |
| 204 class SSLSessionCache { | 204 class SSLSessionCache { |
| 205 public: | 205 public: |
| 206 SSLSessionCache() {} | 206 SSLSessionCache() {} |
| 207 | 207 |
| 208 void OnSessionAdded(const HostPortPair& host_and_port, SSL_SESSION* session) { | 208 void OnSessionAdded(const HostPortPair& host_and_port, |
| 209 const std::string& shard, | |
| 210 SSL_SESSION* session) { | |
| 209 // Declare the session cleaner-upper before the lock, so any call into | 211 // Declare the session cleaner-upper before the lock, so any call into |
| 210 // OpenSSL to free the session will happen after the lock is released. | 212 // OpenSSL to free the session will happen after the lock is released. |
| 211 crypto::ScopedOpenSSL<SSL_SESSION, SSL_SESSION_free> session_to_free; | 213 crypto::ScopedOpenSSL<SSL_SESSION, SSL_SESSION_free> session_to_free; |
| 212 base::AutoLock lock(lock_); | 214 base::AutoLock lock(lock_); |
| 213 | 215 |
| 214 DCHECK_EQ(0U, session_map_.count(session)); | 216 DCHECK_EQ(0U, session_map_.count(session)); |
| 217 const std::string cache_key = GetCacheKey(host_and_port, shard); | |
| 218 | |
| 215 std::pair<HostPortMap::iterator, bool> res = | 219 std::pair<HostPortMap::iterator, bool> res = |
| 216 host_port_map_.insert(std::make_pair(host_and_port, session)); | 220 host_port_map_.insert(std::make_pair(cache_key, session)); |
| 217 if (!res.second) { // Already exists: replace old entry. | 221 if (!res.second) { // Already exists: replace old entry. |
| 218 session_to_free.reset(res.first->second); | 222 session_to_free.reset(res.first->second); |
| 219 session_map_.erase(session_to_free.get()); | 223 session_map_.erase(session_to_free.get()); |
| 220 res.first->second = session; | 224 res.first->second = session; |
| 221 } | 225 } |
| 222 DVLOG(2) << "Adding session " << session << " => " | 226 DVLOG(2) << "Adding session " << session << " => " |
| 223 << host_and_port.ToString() << ", new entry = " << res.second; | 227 << cache_key << ", new entry = " << res.second; |
| 224 DCHECK(host_port_map_[host_and_port] == session); | 228 DCHECK(host_port_map_[cache_key] == session); |
| 225 session_map_[session] = res.first; | 229 session_map_[session] = res.first; |
| 226 DCHECK_EQ(host_port_map_.size(), session_map_.size()); | 230 DCHECK_EQ(host_port_map_.size(), session_map_.size()); |
| 227 DCHECK_LE(host_port_map_.size(), kSessionCacheMaxEntires); | 231 DCHECK_LE(host_port_map_.size(), kSessionCacheMaxEntires); |
| 228 } | 232 } |
| 229 | 233 |
| 230 void OnSessionRemoved(SSL_SESSION* session) { | 234 void OnSessionRemoved(SSL_SESSION* session) { |
| 231 // Declare the session cleaner-upper before the lock, so any call into | 235 // Declare the session cleaner-upper before the lock, so any call into |
| 232 // OpenSSL to free the session will happen after the lock is released. | 236 // OpenSSL to free the session will happen after the lock is released. |
| 233 crypto::ScopedOpenSSL<SSL_SESSION, SSL_SESSION_free> session_to_free; | 237 crypto::ScopedOpenSSL<SSL_SESSION, SSL_SESSION_free> session_to_free; |
| 234 base::AutoLock lock(lock_); | 238 base::AutoLock lock(lock_); |
| 235 | 239 |
| 236 SessionMap::iterator it = session_map_.find(session); | 240 SessionMap::iterator it = session_map_.find(session); |
| 237 if (it == session_map_.end()) | 241 if (it == session_map_.end()) |
| 238 return; | 242 return; |
| 239 DVLOG(2) << "Remove session " << session << " => " | 243 DVLOG(2) << "Remove session " << session << " => " |
| 240 << it->second->first.ToString(); | 244 << it->second->first; |
|
joth
2011/12/09 23:28:37
nit: no need to line break
agl
2011/12/12 22:18:20
Done.
| |
| 241 DCHECK(it->second->second == session); | 245 DCHECK(it->second->second == session); |
| 242 host_port_map_.erase(it->second); | 246 host_port_map_.erase(it->second); |
| 243 session_map_.erase(it); | 247 session_map_.erase(it); |
| 244 session_to_free.reset(session); | 248 session_to_free.reset(session); |
| 245 DCHECK_EQ(host_port_map_.size(), session_map_.size()); | 249 DCHECK_EQ(host_port_map_.size(), session_map_.size()); |
| 246 } | 250 } |
| 247 | 251 |
| 248 // Looks up the host:port in the cache, and if a session is found it is added | 252 // Looks up the host:port in the cache, and if a session is found it is added |
| 249 // to |ssl|, returning true on success. | 253 // to |ssl|, returning true on success. |
| 250 bool SetSSLSession(SSL* ssl, const HostPortPair& host_and_port) { | 254 bool SetSSLSession(SSL* ssl, const HostPortPair& host_and_port, |
| 255 const std::string& shard) { | |
| 251 base::AutoLock lock(lock_); | 256 base::AutoLock lock(lock_); |
| 252 HostPortMap::iterator it = host_port_map_.find(host_and_port); | 257 const std::string cache_key = GetCacheKey(host_and_port, shard); |
| 258 HostPortMap::iterator it = host_port_map_.find(cache_key); | |
| 253 if (it == host_port_map_.end()) | 259 if (it == host_port_map_.end()) |
| 254 return false; | 260 return false; |
| 255 DVLOG(2) << "Lookup session: " << it->second << " => " | 261 DVLOG(2) << "Lookup session: " << it->second << " => " |
| 256 << host_and_port.ToString(); | 262 << cache_key; |
|
joth
2011/12/09 23:28:37
nit: no need for line break
agl
2011/12/12 22:18:20
Done.
| |
| 257 SSL_SESSION* session = it->second; | 263 SSL_SESSION* session = it->second; |
| 258 DCHECK(session); | 264 DCHECK(session); |
| 259 DCHECK(session_map_[session] == it); | 265 DCHECK(session_map_[session] == it); |
| 260 // Ideally we'd release |lock_| before calling into OpenSSL here, however | 266 // Ideally we'd release |lock_| before calling into OpenSSL here, however |
| 261 // that opens a small risk |session| will go out of scope before it is used. | 267 // that opens a small risk |session| will go out of scope before it is used. |
| 262 // Alternatively we would take a temporary local refcount on |session|, | 268 // Alternatively we would take a temporary local refcount on |session|, |
| 263 // except OpenSSL does not provide a public API for adding a ref (c.f. | 269 // except OpenSSL does not provide a public API for adding a ref (c.f. |
| 264 // SSL_SESSION_free which decrements the ref). | 270 // SSL_SESSION_free which decrements the ref). |
| 265 return SSL_set_session(ssl, session) == 1; | 271 return SSL_set_session(ssl, session) == 1; |
| 266 } | 272 } |
| 267 | 273 |
| 274 // Flush removes all entries from the cache. This is called when a client | |
| 275 // certificate is added. | |
| 276 void Flush() { | |
| 277 for (HostPortMap::iterator i = host_port_map_.begin(); | |
| 278 i != host_port_map_.end(); i++) { | |
| 279 SSL_SESSION_free(i->second); | |
| 280 } | |
| 281 host_port_map_.clear(); | |
| 282 session_map_.clear(); | |
| 283 } | |
| 284 | |
| 268 private: | 285 private: |
| 286 static std::string GetCacheKey(const HostPortPair& host_and_port, | |
| 287 const std::string& shard) { | |
| 288 return host_and_port.ToString() + "/" + shard; | |
| 289 } | |
| 290 | |
| 269 // A pair of maps to allow bi-directional lookups between host:port and an | 291 // A pair of maps to allow bi-directional lookups between host:port and an |
| 270 // associated session. | 292 // associated session. |
| 271 // TODO(joth): When client certificates are implemented we should key the | 293 typedef std::map<std::string, SSL_SESSION*> HostPortMap; |
| 272 // cache on the client certificate used in addition to the host-port pair. | |
|
joth
2011/12/09 23:28:37
thanks for handling this one too. if I've followed
agl
2011/12/12 22:18:20
Right. And when we add a new client-cert, we flush
| |
| 273 typedef std::map<HostPortPair, SSL_SESSION*> HostPortMap; | |
| 274 typedef std::map<SSL_SESSION*, HostPortMap::iterator> SessionMap; | 294 typedef std::map<SSL_SESSION*, HostPortMap::iterator> SessionMap; |
| 275 HostPortMap host_port_map_; | 295 HostPortMap host_port_map_; |
| 276 SessionMap session_map_; | 296 SessionMap session_map_; |
| 277 | 297 |
| 278 // Protects access to both the above maps. | 298 // Protects access to both the above maps. |
| 279 base::Lock lock_; | 299 base::Lock lock_; |
| 280 | 300 |
| 281 DISALLOW_COPY_AND_ASSIGN(SSLSessionCache); | 301 DISALLOW_COPY_AND_ASSIGN(SSLSessionCache); |
| 282 }; | 302 }; |
| 283 | 303 |
| (...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 322 NULL); | 342 NULL); |
| 323 #endif | 343 #endif |
| 324 } | 344 } |
| 325 | 345 |
| 326 static int NewSessionCallbackStatic(SSL* ssl, SSL_SESSION* session) { | 346 static int NewSessionCallbackStatic(SSL* ssl, SSL_SESSION* session) { |
| 327 return GetInstance()->NewSessionCallback(ssl, session); | 347 return GetInstance()->NewSessionCallback(ssl, session); |
| 328 } | 348 } |
| 329 | 349 |
| 330 int NewSessionCallback(SSL* ssl, SSL_SESSION* session) { | 350 int NewSessionCallback(SSL* ssl, SSL_SESSION* session) { |
| 331 SSLClientSocketOpenSSL* socket = GetClientSocketFromSSL(ssl); | 351 SSLClientSocketOpenSSL* socket = GetClientSocketFromSSL(ssl); |
| 332 session_cache_.OnSessionAdded(socket->host_and_port(), session); | 352 session_cache_.OnSessionAdded(socket->host_and_port(), |
| 353 socket->session_cache_shard(), | |
| 354 session); | |
| 333 return 1; // 1 => We took ownership of |session|. | 355 return 1; // 1 => We took ownership of |session|. |
| 334 } | 356 } |
| 335 | 357 |
| 336 static void RemoveSessionCallbackStatic(SSL_CTX* ctx, SSL_SESSION* session) { | 358 static void RemoveSessionCallbackStatic(SSL_CTX* ctx, SSL_SESSION* session) { |
| 337 return GetInstance()->RemoveSessionCallback(ctx, session); | 359 return GetInstance()->RemoveSessionCallback(ctx, session); |
| 338 } | 360 } |
| 339 | 361 |
| 340 void RemoveSessionCallback(SSL_CTX* ctx, SSL_SESSION* session) { | 362 void RemoveSessionCallback(SSL_CTX* ctx, SSL_SESSION* session) { |
| 341 DCHECK(ctx == ssl_ctx()); | 363 DCHECK(ctx == ssl_ctx()); |
| 342 session_cache_.OnSessionRemoved(session); | 364 session_cache_.OnSessionRemoved(session); |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 353 const unsigned char* in, | 375 const unsigned char* in, |
| 354 unsigned int inlen, void* arg) { | 376 unsigned int inlen, void* arg) { |
| 355 SSLClientSocketOpenSSL* socket = GetInstance()->GetClientSocketFromSSL(ssl); | 377 SSLClientSocketOpenSSL* socket = GetInstance()->GetClientSocketFromSSL(ssl); |
| 356 return socket->SelectNextProtoCallback(out, outlen, in, inlen); | 378 return socket->SelectNextProtoCallback(out, outlen, in, inlen); |
| 357 } | 379 } |
| 358 | 380 |
| 359 // This is the index used with SSL_get_ex_data to retrieve the owner | 381 // This is the index used with SSL_get_ex_data to retrieve the owner |
| 360 // SSLClientSocketOpenSSL object from an SSL instance. | 382 // SSLClientSocketOpenSSL object from an SSL instance. |
| 361 int ssl_socket_data_index_; | 383 int ssl_socket_data_index_; |
| 362 | 384 |
| 385 // session_cache_ must appear before |ssl_ctx_| because the destruction of | |
| 386 // |ssl_ctx_| may trigger callbacks into |session_cache_|. Therefore, | |
| 387 // |session_cache_| must be destructed after |ssl_ctx_|. | |
| 388 SSLSessionCache session_cache_; | |
| 363 crypto::ScopedOpenSSL<SSL_CTX, SSL_CTX_free> ssl_ctx_; | 389 crypto::ScopedOpenSSL<SSL_CTX, SSL_CTX_free> ssl_ctx_; |
| 364 SSLSessionCache session_cache_; | |
| 365 }; | 390 }; |
| 366 | 391 |
| 367 // Utility to construct the appropriate set & clear masks for use the OpenSSL | 392 // Utility to construct the appropriate set & clear masks for use the OpenSSL |
| 368 // options and mode configuration functions. (SSL_set_options etc) | 393 // options and mode configuration functions. (SSL_set_options etc) |
| 369 struct SslSetClearMask { | 394 struct SslSetClearMask { |
| 370 SslSetClearMask() : set_mask(0), clear_mask(0) {} | 395 SslSetClearMask() : set_mask(0), clear_mask(0) {} |
| 371 void ConfigureFlag(long flag, bool state) { | 396 void ConfigureFlag(long flag, bool state) { |
| 372 (state ? set_mask : clear_mask) |= flag; | 397 (state ? set_mask : clear_mask) |= flag; |
| 373 // Make sure we haven't got any intersection in the set & clear options. | 398 // Make sure we haven't got any intersection in the set & clear options. |
| 374 DCHECK_EQ(0, set_mask & clear_mask) << flag << ":" << state; | 399 DCHECK_EQ(0, set_mask & clear_mask) << flag << ":" << state; |
| 375 } | 400 } |
| 376 long set_mask; | 401 long set_mask; |
| 377 long clear_mask; | 402 long clear_mask; |
| 378 }; | 403 }; |
| 379 | 404 |
| 380 } // namespace | 405 } // namespace |
| 381 | 406 |
| 407 // static | |
| 408 void SSLClientSocket::ClearSessionCache() { | |
| 409 SSLContext* context = SSLContext::GetInstance(); | |
| 410 context->session_cache()->Flush(); | |
| 411 } | |
| 412 | |
| 382 SSLClientSocketOpenSSL::SSLClientSocketOpenSSL( | 413 SSLClientSocketOpenSSL::SSLClientSocketOpenSSL( |
| 383 ClientSocketHandle* transport_socket, | 414 ClientSocketHandle* transport_socket, |
| 384 const HostPortPair& host_and_port, | 415 const HostPortPair& host_and_port, |
| 385 const SSLConfig& ssl_config, | 416 const SSLConfig& ssl_config, |
| 386 const SSLClientSocketContext& context) | 417 const SSLClientSocketContext& context) |
| 387 : ALLOW_THIS_IN_INITIALIZER_LIST(buffer_send_callback_( | 418 : ALLOW_THIS_IN_INITIALIZER_LIST(buffer_send_callback_( |
| 388 this, &SSLClientSocketOpenSSL::BufferSendComplete)), | 419 this, &SSLClientSocketOpenSSL::BufferSendComplete)), |
| 389 ALLOW_THIS_IN_INITIALIZER_LIST(buffer_recv_callback_( | 420 ALLOW_THIS_IN_INITIALIZER_LIST(buffer_recv_callback_( |
| 390 this, &SSLClientSocketOpenSSL::BufferRecvComplete)), | 421 this, &SSLClientSocketOpenSSL::BufferRecvComplete)), |
| 391 transport_send_busy_(false), | 422 transport_send_busy_(false), |
| 392 transport_recv_busy_(false), | 423 transport_recv_busy_(false), |
| 393 old_user_connect_callback_(NULL), | 424 old_user_connect_callback_(NULL), |
| 394 old_user_read_callback_(NULL), | 425 old_user_read_callback_(NULL), |
| 395 user_write_callback_(NULL), | 426 user_write_callback_(NULL), |
| 396 completed_handshake_(false), | 427 completed_handshake_(false), |
| 397 client_auth_cert_needed_(false), | 428 client_auth_cert_needed_(false), |
| 398 cert_verifier_(context.cert_verifier), | 429 cert_verifier_(context.cert_verifier), |
| 399 ssl_(NULL), | 430 ssl_(NULL), |
| 400 transport_bio_(NULL), | 431 transport_bio_(NULL), |
| 401 transport_(transport_socket), | 432 transport_(transport_socket), |
| 402 host_and_port_(host_and_port), | 433 host_and_port_(host_and_port), |
| 403 ssl_config_(ssl_config), | 434 ssl_config_(ssl_config), |
| 435 session_cache_shard_(context.session_cache_shard), | |
| 404 trying_cached_session_(false), | 436 trying_cached_session_(false), |
| 405 npn_status_(kNextProtoUnsupported), | 437 npn_status_(kNextProtoUnsupported), |
| 406 net_log_(transport_socket->socket()->NetLog()) { | 438 net_log_(transport_socket->socket()->NetLog()) { |
| 407 } | 439 } |
| 408 | 440 |
| 409 SSLClientSocketOpenSSL::~SSLClientSocketOpenSSL() { | 441 SSLClientSocketOpenSSL::~SSLClientSocketOpenSSL() { |
| 410 Disconnect(); | 442 Disconnect(); |
| 411 } | 443 } |
| 412 | 444 |
| 413 bool SSLClientSocketOpenSSL::Init() { | 445 bool SSLClientSocketOpenSSL::Init() { |
| 414 DCHECK(!ssl_); | 446 DCHECK(!ssl_); |
| 415 DCHECK(!transport_bio_); | 447 DCHECK(!transport_bio_); |
| 416 | 448 |
| 417 SSLContext* context = SSLContext::GetInstance(); | 449 SSLContext* context = SSLContext::GetInstance(); |
| 418 crypto::OpenSSLErrStackTracer err_tracer(FROM_HERE); | 450 crypto::OpenSSLErrStackTracer err_tracer(FROM_HERE); |
| 419 | 451 |
| 420 ssl_ = SSL_new(context->ssl_ctx()); | 452 ssl_ = SSL_new(context->ssl_ctx()); |
| 421 if (!ssl_ || !context->SetClientSocketForSSL(ssl_, this)) | 453 if (!ssl_ || !context->SetClientSocketForSSL(ssl_, this)) |
| 422 return false; | 454 return false; |
| 423 | 455 |
| 424 if (!SSL_set_tlsext_host_name(ssl_, host_and_port_.host().c_str())) | 456 if (!SSL_set_tlsext_host_name(ssl_, host_and_port_.host().c_str())) |
| 425 return false; | 457 return false; |
| 426 | 458 |
| 427 trying_cached_session_ = | 459 trying_cached_session_ = |
| 428 context->session_cache()->SetSSLSession(ssl_, host_and_port_); | 460 context->session_cache()->SetSSLSession(ssl_, host_and_port_, |
| 461 session_cache_shard_); | |
| 429 | 462 |
| 430 BIO* ssl_bio = NULL; | 463 BIO* ssl_bio = NULL; |
| 431 // 0 => use default buffer sizes. | 464 // 0 => use default buffer sizes. |
| 432 if (!BIO_new_bio_pair(&ssl_bio, 0, &transport_bio_, 0)) | 465 if (!BIO_new_bio_pair(&ssl_bio, 0, &transport_bio_, 0)) |
| 433 return false; | 466 return false; |
| 434 DCHECK(ssl_bio); | 467 DCHECK(ssl_bio); |
| 435 DCHECK(transport_bio_); | 468 DCHECK(transport_bio_); |
| 436 | 469 |
| 437 SSL_set_bio(ssl_, ssl_bio, ssl_bio); | 470 SSL_set_bio(ssl_, ssl_bio, ssl_bio); |
| 438 | 471 |
| (...skipping 244 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 683 user_connect_callback_ = callback; | 716 user_connect_callback_ = callback; |
| 684 } else { | 717 } else { |
| 685 net_log_.EndEventWithNetErrorCode(NetLog::TYPE_SSL_CONNECT, rv); | 718 net_log_.EndEventWithNetErrorCode(NetLog::TYPE_SSL_CONNECT, rv); |
| 686 } | 719 } |
| 687 | 720 |
| 688 return rv > OK ? OK : rv; | 721 return rv > OK ? OK : rv; |
| 689 } | 722 } |
| 690 | 723 |
| 691 void SSLClientSocketOpenSSL::Disconnect() { | 724 void SSLClientSocketOpenSSL::Disconnect() { |
| 692 if (ssl_) { | 725 if (ssl_) { |
| 726 // Calling SSL_shutdown prevents the session from being marked as | |
| 727 // unresumable. | |
| 728 SSL_shutdown(ssl_); | |
| 693 SSL_free(ssl_); | 729 SSL_free(ssl_); |
| 694 ssl_ = NULL; | 730 ssl_ = NULL; |
| 695 } | 731 } |
| 696 if (transport_bio_) { | 732 if (transport_bio_) { |
| 697 BIO_free_all(transport_bio_); | 733 BIO_free_all(transport_bio_); |
| 698 transport_bio_ = NULL; | 734 transport_bio_ = NULL; |
| 699 } | 735 } |
| 700 | 736 |
| 701 // Shut down anything that may call us back. | 737 // Shut down anything that may call us back. |
| 702 verifier_.reset(); | 738 verifier_.reset(); |
| (...skipping 600 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1303 net_log_.AddByteTransferEvent(NetLog::TYPE_SSL_SOCKET_BYTES_SENT, rv, | 1339 net_log_.AddByteTransferEvent(NetLog::TYPE_SSL_SOCKET_BYTES_SENT, rv, |
| 1304 user_write_buf_->data()); | 1340 user_write_buf_->data()); |
| 1305 return rv; | 1341 return rv; |
| 1306 } | 1342 } |
| 1307 | 1343 |
| 1308 int err = SSL_get_error(ssl_, rv); | 1344 int err = SSL_get_error(ssl_, rv); |
| 1309 return MapOpenSSLError(err, err_tracer); | 1345 return MapOpenSSLError(err, err_tracer); |
| 1310 } | 1346 } |
| 1311 | 1347 |
| 1312 } // namespace net | 1348 } // namespace net |
| OLD | NEW |