Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(261)

Side by Side Diff: net/socket/ssl_client_socket_openssl.cc

Issue 89623002: net: Implement new SSL session cache for OpenSSL sockets. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Update comments Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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 // 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/err.h> 10 #include <openssl/err.h>
11 #include <openssl/opensslv.h> 11 #include <openssl/opensslv.h>
12 #include <openssl/ssl.h> 12 #include <openssl/ssl.h>
13 13
14 #include "base/bind.h" 14 #include "base/bind.h"
15 #include "base/callback_helpers.h" 15 #include "base/callback_helpers.h"
16 #include "base/memory/singleton.h" 16 #include "base/memory/singleton.h"
17 #include "base/metrics/histogram.h" 17 #include "base/metrics/histogram.h"
18 #include "base/synchronization/lock.h" 18 #include "base/synchronization/lock.h"
19 #include "crypto/ec_private_key.h" 19 #include "crypto/ec_private_key.h"
20 #include "crypto/openssl_util.h" 20 #include "crypto/openssl_util.h"
21 #include "net/base/net_errors.h" 21 #include "net/base/net_errors.h"
22 #include "net/cert/cert_verifier.h" 22 #include "net/cert/cert_verifier.h"
23 #include "net/cert/single_request_cert_verifier.h" 23 #include "net/cert/single_request_cert_verifier.h"
24 #include "net/cert/x509_certificate_net_log_param.h" 24 #include "net/cert/x509_certificate_net_log_param.h"
25 #include "net/socket/ssl_error_params.h" 25 #include "net/socket/ssl_error_params.h"
26 #include "net/socket/ssl_session_cache_openssl.h"
26 #include "net/ssl/openssl_client_key_store.h" 27 #include "net/ssl/openssl_client_key_store.h"
27 #include "net/ssl/ssl_cert_request_info.h" 28 #include "net/ssl/ssl_cert_request_info.h"
28 #include "net/ssl/ssl_connection_status_flags.h" 29 #include "net/ssl/ssl_connection_status_flags.h"
29 #include "net/ssl/ssl_info.h" 30 #include "net/ssl/ssl_info.h"
30 31
31 namespace net { 32 namespace net {
32 33
33 namespace { 34 namespace {
34 35
35 // Enable this to see logging for state machine state transitions. 36 // Enable this to see logging for state machine state transitions.
36 #if 0 37 #if 0
37 #define GotoState(s) do { DVLOG(2) << (void *)this << " " << __FUNCTION__ << \ 38 #define GotoState(s) do { DVLOG(2) << (void *)this << " " << __FUNCTION__ << \
38 " jump to state " << s; \ 39 " jump to state " << s; \
39 next_handshake_state_ = s; } while (0) 40 next_handshake_state_ = s; } while (0)
40 #else 41 #else
41 #define GotoState(s) next_handshake_state_ = s 42 #define GotoState(s) next_handshake_state_ = s
42 #endif 43 #endif
43 44
44 const int kSessionCacheTimeoutSeconds = 60 * 60;
45 const size_t kSessionCacheMaxEntires = 1024;
46
47 // This constant can be any non-negative/non-zero value (eg: it does not 45 // This constant can be any non-negative/non-zero value (eg: it does not
48 // overlap with any value of the net::Error range, including net::OK). 46 // overlap with any value of the net::Error range, including net::OK).
49 const int kNoPendingReadResult = 1; 47 const int kNoPendingReadResult = 1;
50 48
51 // If a client doesn't have a list of protocols that it supports, but 49 // If a client doesn't have a list of protocols that it supports, but
52 // the server supports NPN, choosing "http/1.1" is the best answer. 50 // the server supports NPN, choosing "http/1.1" is the best answer.
53 const char kDefaultSupportedNPNProtocol[] = "http/1.1"; 51 const char kDefaultSupportedNPNProtocol[] = "http/1.1";
54 52
55 #if OPENSSL_VERSION_NUMBER < 0x1000103fL 53 #if OPENSSL_VERSION_NUMBER < 0x1000103fL
56 // This method doesn't seem to have made it into the OpenSSL headers. 54 // This method doesn't seem to have made it into the OpenSSL headers.
(...skipping 151 matching lines...) Expand 10 before | Expand all | Expand 10 after
208 } 206 }
209 } 207 }
210 208
211 // We do certificate verification after handshake, so we disable the default 209 // We do certificate verification after handshake, so we disable the default
212 // by registering a no-op verify function. 210 // by registering a no-op verify function.
213 int NoOpVerifyCallback(X509_STORE_CTX*, void *) { 211 int NoOpVerifyCallback(X509_STORE_CTX*, void *) {
214 DVLOG(3) << "skipping cert verify"; 212 DVLOG(3) << "skipping cert verify";
215 return 1; 213 return 1;
216 } 214 }
217 215
218 // OpenSSL manages a cache of SSL_SESSION, this class provides the application
219 // side policy for that cache about session re-use: we retain one session per
220 // unique HostPortPair, per shard.
221 class SSLSessionCache {
222 public:
223 SSLSessionCache() {}
224
225 void OnSessionAdded(const HostPortPair& host_and_port,
226 const std::string& shard,
227 SSL_SESSION* session) {
228 // Declare the session cleaner-upper before the lock, so any call into
229 // OpenSSL to free the session will happen after the lock is released.
230 crypto::ScopedOpenSSL<SSL_SESSION, SSL_SESSION_free> session_to_free;
231 base::AutoLock lock(lock_);
232
233 DCHECK_EQ(0U, session_map_.count(session));
234 const std::string cache_key = GetCacheKey(host_and_port, shard);
235
236 std::pair<HostPortMap::iterator, bool> res =
237 host_port_map_.insert(std::make_pair(cache_key, session));
238 if (!res.second) { // Already exists: replace old entry.
239 session_to_free.reset(res.first->second);
240 session_map_.erase(session_to_free.get());
241 res.first->second = session;
242 }
243 DVLOG(2) << "Adding session " << session << " => "
244 << cache_key << ", new entry = " << res.second;
245 DCHECK(host_port_map_[cache_key] == session);
246 session_map_[session] = res.first;
247 DCHECK_EQ(host_port_map_.size(), session_map_.size());
248 DCHECK_LE(host_port_map_.size(), kSessionCacheMaxEntires);
249 }
250
251 void OnSessionRemoved(SSL_SESSION* session) {
252 // Declare the session cleaner-upper before the lock, so any call into
253 // OpenSSL to free the session will happen after the lock is released.
254 crypto::ScopedOpenSSL<SSL_SESSION, SSL_SESSION_free> session_to_free;
255 base::AutoLock lock(lock_);
256
257 SessionMap::iterator it = session_map_.find(session);
258 if (it == session_map_.end())
259 return;
260 DVLOG(2) << "Remove session " << session << " => " << it->second->first;
261 DCHECK(it->second->second == session);
262 host_port_map_.erase(it->second);
263 session_map_.erase(it);
264 session_to_free.reset(session);
265 DCHECK_EQ(host_port_map_.size(), session_map_.size());
266 }
267
268 // Looks up the host:port in the cache, and if a session is found it is added
269 // to |ssl|, returning true on success.
270 bool SetSSLSession(SSL* ssl, const HostPortPair& host_and_port,
271 const std::string& shard) {
272 base::AutoLock lock(lock_);
273 const std::string cache_key = GetCacheKey(host_and_port, shard);
274 HostPortMap::iterator it = host_port_map_.find(cache_key);
275 if (it == host_port_map_.end())
276 return false;
277 DVLOG(2) << "Lookup session: " << it->second << " => " << cache_key;
278 SSL_SESSION* session = it->second;
279 DCHECK(session);
280 DCHECK(session_map_[session] == it);
281 // Ideally we'd release |lock_| before calling into OpenSSL here, however
282 // that opens a small risk |session| will go out of scope before it is used.
283 // Alternatively we would take a temporary local refcount on |session|,
284 // except OpenSSL does not provide a public API for adding a ref (c.f.
285 // SSL_SESSION_free which decrements the ref).
286 return SSL_set_session(ssl, session) == 1;
287 }
288
289 // Flush removes all entries from the cache. This is called when a client
290 // certificate is added.
291 void Flush() {
292 base::AutoLock lock(lock_);
293 for (HostPortMap::iterator i = host_port_map_.begin();
294 i != host_port_map_.end(); i++) {
295 SSL_SESSION_free(i->second);
296 }
297 host_port_map_.clear();
298 session_map_.clear();
299 }
300
301 private:
302 static std::string GetCacheKey(const HostPortPair& host_and_port,
303 const std::string& shard) {
304 return host_and_port.ToString() + "/" + shard;
305 }
306
307 // A pair of maps to allow bi-directional lookups between host:port and an
308 // associated session.
309 typedef std::map<std::string, SSL_SESSION*> HostPortMap;
310 typedef std::map<SSL_SESSION*, HostPortMap::iterator> SessionMap;
311 HostPortMap host_port_map_;
312 SessionMap session_map_;
313
314 // Protects access to both the above maps.
315 base::Lock lock_;
316
317 DISALLOW_COPY_AND_ASSIGN(SSLSessionCache);
318 };
319
320 // Utility to construct the appropriate set & clear masks for use the OpenSSL 216 // Utility to construct the appropriate set & clear masks for use the OpenSSL
321 // options and mode configuration functions. (SSL_set_options etc) 217 // options and mode configuration functions. (SSL_set_options etc)
322 struct SslSetClearMask { 218 struct SslSetClearMask {
323 SslSetClearMask() : set_mask(0), clear_mask(0) {} 219 SslSetClearMask() : set_mask(0), clear_mask(0) {}
324 void ConfigureFlag(long flag, bool state) { 220 void ConfigureFlag(long flag, bool state) {
325 (state ? set_mask : clear_mask) |= flag; 221 (state ? set_mask : clear_mask) |= flag;
326 // Make sure we haven't got any intersection in the set & clear options. 222 // Make sure we haven't got any intersection in the set & clear options.
327 DCHECK_EQ(0, set_mask & clear_mask) << flag << ":" << state; 223 DCHECK_EQ(0, set_mask & clear_mask) << flag << ":" << state;
328 } 224 }
329 long set_mask; 225 long set_mask;
330 long clear_mask; 226 long clear_mask;
331 }; 227 };
332 228
229 // Compute a unique key string for the SSL session cache. |socket| is an
230 // input socket object. Return a string.
231 std::string GetSocketSessionCacheKey(const SSLClientSocketOpenSSL& socket) {
232 std::string result = socket.host_and_port().ToString();
233 result.append("/");
234 result.append(socket.ssl_session_cache_shard());
235 return result;
236 }
237
333 } // namespace 238 } // namespace
334 239
335 class SSLClientSocketOpenSSL::SSLContext { 240 class SSLClientSocketOpenSSL::SSLContext {
336 public: 241 public:
337 static SSLContext* GetInstance() { return Singleton<SSLContext>::get(); } 242 static SSLContext* GetInstance() { return Singleton<SSLContext>::get(); }
338 SSL_CTX* ssl_ctx() { return ssl_ctx_.get(); } 243 SSL_CTX* ssl_ctx() { return ssl_ctx_.get(); }
339 SSLSessionCache* session_cache() { return &session_cache_; } 244 SSLSessionCacheOpenSSL* session_cache() { return &session_cache_; }
340 245
341 SSLClientSocketOpenSSL* GetClientSocketFromSSL(SSL* ssl) { 246 SSLClientSocketOpenSSL* GetClientSocketFromSSL(const SSL* ssl) {
342 DCHECK(ssl); 247 DCHECK(ssl);
343 SSLClientSocketOpenSSL* socket = static_cast<SSLClientSocketOpenSSL*>( 248 SSLClientSocketOpenSSL* socket = static_cast<SSLClientSocketOpenSSL*>(
344 SSL_get_ex_data(ssl, ssl_socket_data_index_)); 249 SSL_get_ex_data(ssl, ssl_socket_data_index_));
345 DCHECK(socket); 250 DCHECK(socket);
346 return socket; 251 return socket;
347 } 252 }
348 253
349 bool SetClientSocketForSSL(SSL* ssl, SSLClientSocketOpenSSL* socket) { 254 bool SetClientSocketForSSL(SSL* ssl, SSLClientSocketOpenSSL* socket) {
350 return SSL_set_ex_data(ssl, ssl_socket_data_index_, socket) != 0; 255 return SSL_set_ex_data(ssl, ssl_socket_data_index_, socket) != 0;
351 } 256 }
352 257
353 private: 258 private:
354 friend struct DefaultSingletonTraits<SSLContext>; 259 friend struct DefaultSingletonTraits<SSLContext>;
355 260
356 SSLContext() { 261 SSLContext() {
357 crypto::EnsureOpenSSLInit(); 262 crypto::EnsureOpenSSLInit();
358 ssl_socket_data_index_ = SSL_get_ex_new_index(0, 0, 0, 0, 0); 263 ssl_socket_data_index_ = SSL_get_ex_new_index(0, 0, 0, 0, 0);
359 DCHECK_NE(ssl_socket_data_index_, -1); 264 DCHECK_NE(ssl_socket_data_index_, -1);
360 ssl_ctx_.reset(SSL_CTX_new(SSLv23_client_method())); 265 ssl_ctx_.reset(SSL_CTX_new(SSLv23_client_method()));
266 session_cache_.Reset(ssl_ctx_.get(), kDefaultSessionCacheConfig);
361 SSL_CTX_set_cert_verify_callback(ssl_ctx_.get(), NoOpVerifyCallback, NULL); 267 SSL_CTX_set_cert_verify_callback(ssl_ctx_.get(), NoOpVerifyCallback, NULL);
362 SSL_CTX_set_session_cache_mode(ssl_ctx_.get(), SSL_SESS_CACHE_CLIENT);
363 SSL_CTX_sess_set_new_cb(ssl_ctx_.get(), NewSessionCallbackStatic);
364 SSL_CTX_sess_set_remove_cb(ssl_ctx_.get(), RemoveSessionCallbackStatic);
365 SSL_CTX_set_timeout(ssl_ctx_.get(), kSessionCacheTimeoutSeconds);
366 SSL_CTX_sess_set_cache_size(ssl_ctx_.get(), kSessionCacheMaxEntires);
367 SSL_CTX_set_client_cert_cb(ssl_ctx_.get(), ClientCertCallback); 268 SSL_CTX_set_client_cert_cb(ssl_ctx_.get(), ClientCertCallback);
368 SSL_CTX_set_channel_id_cb(ssl_ctx_.get(), ChannelIDCallback); 269 SSL_CTX_set_channel_id_cb(ssl_ctx_.get(), ChannelIDCallback);
369 #if defined(OPENSSL_NPN_NEGOTIATED) 270 #if defined(OPENSSL_NPN_NEGOTIATED)
370 // TODO(kristianm): Only select this if ssl_config_.next_proto is not empty. 271 // TODO(kristianm): Only select this if ssl_config_.next_proto is not empty.
371 // It would be better if the callback were not a global setting, 272 // It would be better if the callback were not a global setting,
372 // but that is an OpenSSL issue. 273 // but that is an OpenSSL issue.
373 SSL_CTX_set_next_proto_select_cb(ssl_ctx_.get(), SelectNextProtoCallback, 274 SSL_CTX_set_next_proto_select_cb(ssl_ctx_.get(), SelectNextProtoCallback,
374 NULL); 275 NULL);
375 #endif 276 #endif
376 } 277 }
377 278
378 static int NewSessionCallbackStatic(SSL* ssl, SSL_SESSION* session) { 279 static std::string GetSessionCacheKey(const SSL* ssl) {
379 return GetInstance()->NewSessionCallback(ssl, session); 280 SSLClientSocketOpenSSL* socket = GetInstance()->GetClientSocketFromSSL(ssl);
281 DCHECK(socket);
282 return GetSocketSessionCacheKey(*socket);
380 } 283 }
381 284
382 int NewSessionCallback(SSL* ssl, SSL_SESSION* session) { 285 static SSLSessionCacheOpenSSL::Config kDefaultSessionCacheConfig;
383 SSLClientSocketOpenSSL* socket = GetClientSocketFromSSL(ssl);
384 session_cache_.OnSessionAdded(socket->host_and_port(),
385 socket->ssl_session_cache_shard(),
386 session);
387 return 1; // 1 => We took ownership of |session|.
388 }
389
390 static void RemoveSessionCallbackStatic(SSL_CTX* ctx, SSL_SESSION* session) {
391 return GetInstance()->RemoveSessionCallback(ctx, session);
392 }
393
394 void RemoveSessionCallback(SSL_CTX* ctx, SSL_SESSION* session) {
395 DCHECK(ctx == ssl_ctx());
396 session_cache_.OnSessionRemoved(session);
397 }
398 286
399 static int ClientCertCallback(SSL* ssl, X509** x509, EVP_PKEY** pkey) { 287 static int ClientCertCallback(SSL* ssl, X509** x509, EVP_PKEY** pkey) {
400 SSLClientSocketOpenSSL* socket = GetInstance()->GetClientSocketFromSSL(ssl); 288 SSLClientSocketOpenSSL* socket = GetInstance()->GetClientSocketFromSSL(ssl);
401 CHECK(socket); 289 CHECK(socket);
402 return socket->ClientCertRequestCallback(ssl, x509, pkey); 290 return socket->ClientCertRequestCallback(ssl, x509, pkey);
403 } 291 }
404 292
405 static void ChannelIDCallback(SSL* ssl, EVP_PKEY** pkey) { 293 static void ChannelIDCallback(SSL* ssl, EVP_PKEY** pkey) {
406 SSLClientSocketOpenSSL* socket = GetInstance()->GetClientSocketFromSSL(ssl); 294 SSLClientSocketOpenSSL* socket = GetInstance()->GetClientSocketFromSSL(ssl);
407 CHECK(socket); 295 CHECK(socket);
408 socket->ChannelIDRequestCallback(ssl, pkey); 296 socket->ChannelIDRequestCallback(ssl, pkey);
409 } 297 }
410 298
411 static int SelectNextProtoCallback(SSL* ssl, 299 static int SelectNextProtoCallback(SSL* ssl,
412 unsigned char** out, unsigned char* outlen, 300 unsigned char** out, unsigned char* outlen,
413 const unsigned char* in, 301 const unsigned char* in,
414 unsigned int inlen, void* arg) { 302 unsigned int inlen, void* arg) {
415 SSLClientSocketOpenSSL* socket = GetInstance()->GetClientSocketFromSSL(ssl); 303 SSLClientSocketOpenSSL* socket = GetInstance()->GetClientSocketFromSSL(ssl);
416 return socket->SelectNextProtoCallback(out, outlen, in, inlen); 304 return socket->SelectNextProtoCallback(out, outlen, in, inlen);
417 } 305 }
418 306
419 // This is the index used with SSL_get_ex_data to retrieve the owner 307 // This is the index used with SSL_get_ex_data to retrieve the owner
420 // SSLClientSocketOpenSSL object from an SSL instance. 308 // SSLClientSocketOpenSSL object from an SSL instance.
421 int ssl_socket_data_index_; 309 int ssl_socket_data_index_;
422 310
423 // session_cache_ must appear before |ssl_ctx_| because the destruction of
424 // |ssl_ctx_| may trigger callbacks into |session_cache_|. Therefore,
425 // |session_cache_| must be destructed after |ssl_ctx_|.
426 SSLSessionCache session_cache_;
427 crypto::ScopedOpenSSL<SSL_CTX, SSL_CTX_free> ssl_ctx_; 311 crypto::ScopedOpenSSL<SSL_CTX, SSL_CTX_free> ssl_ctx_;
312 // |session_cache_| must be destroyed before |ssl_ctx_|.
313 SSLSessionCacheOpenSSL session_cache_;
428 }; 314 };
429 315
430 // static 316 // static
317 SSLSessionCacheOpenSSL::Config
318 SSLClientSocketOpenSSL::SSLContext::kDefaultSessionCacheConfig = {
319 &GetSessionCacheKey, // key_func
320 1024, // max_entries
321 256, // expiration_check_count
322 60 * 60, // timeout_seconds
323 };
324
325 // static
431 void SSLClientSocket::ClearSessionCache() { 326 void SSLClientSocket::ClearSessionCache() {
432 SSLClientSocketOpenSSL::SSLContext* context = 327 SSLClientSocketOpenSSL::SSLContext* context =
433 SSLClientSocketOpenSSL::SSLContext::GetInstance(); 328 SSLClientSocketOpenSSL::SSLContext::GetInstance();
434 context->session_cache()->Flush(); 329 context->session_cache()->Flush();
435 } 330 }
436 331
437 SSLClientSocketOpenSSL::SSLClientSocketOpenSSL( 332 SSLClientSocketOpenSSL::SSLClientSocketOpenSSL(
438 scoped_ptr<ClientSocketHandle> transport_socket, 333 scoped_ptr<ClientSocketHandle> transport_socket,
439 const HostPortPair& host_and_port, 334 const HostPortPair& host_and_port,
440 const SSLConfig& ssl_config, 335 const SSLConfig& ssl_config,
(...skipping 305 matching lines...) Expand 10 before | Expand all | Expand 10 after
746 SSLContext* context = SSLContext::GetInstance(); 641 SSLContext* context = SSLContext::GetInstance();
747 crypto::OpenSSLErrStackTracer err_tracer(FROM_HERE); 642 crypto::OpenSSLErrStackTracer err_tracer(FROM_HERE);
748 643
749 ssl_ = SSL_new(context->ssl_ctx()); 644 ssl_ = SSL_new(context->ssl_ctx());
750 if (!ssl_ || !context->SetClientSocketForSSL(ssl_, this)) 645 if (!ssl_ || !context->SetClientSocketForSSL(ssl_, this))
751 return false; 646 return false;
752 647
753 if (!SSL_set_tlsext_host_name(ssl_, host_and_port_.host().c_str())) 648 if (!SSL_set_tlsext_host_name(ssl_, host_and_port_.host().c_str()))
754 return false; 649 return false;
755 650
756 trying_cached_session_ = 651 trying_cached_session_ = context->session_cache()->SetSSLSessionWithKey(
757 context->session_cache()->SetSSLSession(ssl_, host_and_port_, 652 ssl_, GetSocketSessionCacheKey(*this));
758 ssl_session_cache_shard_);
759 653
760 BIO* ssl_bio = NULL; 654 BIO* ssl_bio = NULL;
761 // 0 => use default buffer sizes. 655 // 0 => use default buffer sizes.
762 if (!BIO_new_bio_pair(&ssl_bio, 0, &transport_bio_, 0)) 656 if (!BIO_new_bio_pair(&ssl_bio, 0, &transport_bio_, 0))
763 return false; 657 return false;
764 DCHECK(ssl_bio); 658 DCHECK(ssl_bio);
765 DCHECK(transport_bio_); 659 DCHECK(transport_bio_);
766 660
767 SSL_set_bio(ssl_, ssl_bio, ssl_bio); 661 SSL_set_bio(ssl_, ssl_bio, ssl_bio);
768 662
(...skipping 723 matching lines...) Expand 10 before | Expand all | Expand 10 after
1492 } 1386 }
1493 1387
1494 npn_proto_.assign(reinterpret_cast<const char*>(*out), *outlen); 1388 npn_proto_.assign(reinterpret_cast<const char*>(*out), *outlen);
1495 server_protos_.assign(reinterpret_cast<const char*>(in), inlen); 1389 server_protos_.assign(reinterpret_cast<const char*>(in), inlen);
1496 DVLOG(2) << "next protocol: '" << npn_proto_ << "' status: " << npn_status_; 1390 DVLOG(2) << "next protocol: '" << npn_proto_ << "' status: " << npn_status_;
1497 #endif 1391 #endif
1498 return SSL_TLSEXT_ERR_OK; 1392 return SSL_TLSEXT_ERR_OK;
1499 } 1393 }
1500 1394
1501 } // namespace net 1395 } // namespace net
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698