| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/network/ssl_config_service_mojo.h" |
| |
| #include "base/files/file_util.h" |
| #include "base/run_loop.h" |
| #include "base/stl_util.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "build/build_config.h" |
| #include "crypto/sha2.h" |
| #include "mojo/public/cpp/bindings/interface_request.h" |
| #include "net/base/test_completion_callback.h" |
| #include "net/cert/asn1_util.h" |
| #include "net/cert/cert_verifier.h" |
| #include "net/cert/cert_verify_result.h" |
| #include "net/cert/crl_set.h" |
| #include "net/cert/test_root_certs.h" |
| #include "net/cert/x509_certificate.h" |
| #include "net/cert/x509_util.h" |
| #include "net/log/net_log_with_source.h" |
| #include "net/ssl/ssl_config.h" |
| #include "net/ssl/ssl_config_service.h" |
| #include "net/test/cert_test_util.h" |
| #include "net/test/gtest_util.h" |
| #include "net/test/test_data_directory.h" |
| #include "net/url_request/url_request_context.h" |
| #include "services/network/network_context.h" |
| #include "services/network/network_service.h" |
| #include "services/network/public/mojom/network_service.mojom.h" |
| #include "services/network/public/mojom/ssl_config.mojom.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace network { |
| namespace { |
| |
| class TestSSLConfigServiceObserver : public net::SSLConfigService::Observer { |
| public: |
| explicit TestSSLConfigServiceObserver( |
| net::SSLConfigService* ssl_config_service) |
| : ssl_config_service_(ssl_config_service) { |
| ssl_config_service_->AddObserver(this); |
| } |
| |
| ~TestSSLConfigServiceObserver() override { |
| EXPECT_EQ(observed_changes_, changes_to_wait_for_); |
| ssl_config_service_->RemoveObserver(this); |
| } |
| |
| // net::SSLConfigService::Observer implementation: |
| void OnSSLConfigChanged() override { |
| ++observed_changes_; |
| ssl_config_service_->GetSSLConfig(&ssl_config_during_change_); |
| if (run_loop_) |
| run_loop_->Quit(); |
| } |
| |
| // Waits for a SSLConfig change. The first time it's called, waits for the |
| // first change, if one hasn't been observed already, the second time, waits |
| // for the second, etc. Must be called once for each change that happens, and |
| // fails it more than once change happens between calls, or during a call. |
| void WaitForChange() { |
| EXPECT_FALSE(run_loop_); |
| ++changes_to_wait_for_; |
| if (changes_to_wait_for_ == observed_changes_) |
| return; |
| EXPECT_LT(observed_changes_, changes_to_wait_for_); |
| |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| run_loop_->Run(); |
| run_loop_.reset(); |
| EXPECT_EQ(observed_changes_, changes_to_wait_for_); |
| } |
| |
| const net::SSLConfig& ssl_config_during_change() const { |
| return ssl_config_during_change_; |
| } |
| |
| int observed_changes() const { return observed_changes_; } |
| |
| private: |
| net::SSLConfigService* const ssl_config_service_; |
| int observed_changes_ = 0; |
| int changes_to_wait_for_ = 0; |
| net::SSLConfig ssl_config_during_change_; |
| std::unique_ptr<base::RunLoop> run_loop_; |
| }; |
| |
| class TestCertVerifierConfigObserver : public net::CertVerifier { |
| public: |
| TestCertVerifierConfigObserver() = default; |
| ~TestCertVerifierConfigObserver() override { |
| EXPECT_EQ(observed_changes_, changes_to_wait_for_); |
| } |
| |
| // CertVerifier implementation: |
| int Verify(const net::CertVerifier::RequestParams& params, |
| net::CertVerifyResult* verify_result, |
| net::CompletionOnceCallback callback, |
| std::unique_ptr<net::CertVerifier::Request>* out_req, |
| const net::NetLogWithSource& net_log) override { |
| ADD_FAILURE() << "Verify should not be called by tests"; |
| return net::ERR_FAILED; |
| } |
| void SetConfig(const Config& config) override { |
| ++observed_changes_; |
| verifier_config_during_change_ = config; |
| if (run_loop_) |
| run_loop_->Quit(); |
| } |
| |
| // Waits for a SSLConfig change. The first time it's called, waits for the |
| // first change, if one hasn't been observed already, the second time, waits |
| // for the second, etc. Must be called once for each change that happens, and |
| // fails it more than once change happens between calls, or during a call. |
| void WaitForChange() { |
| EXPECT_FALSE(run_loop_); |
| ++changes_to_wait_for_; |
| if (changes_to_wait_for_ == observed_changes_) |
| return; |
| EXPECT_LT(observed_changes_, changes_to_wait_for_); |
| |
| run_loop_ = std::make_unique<base::RunLoop>(); |
| run_loop_->Run(); |
| run_loop_.reset(); |
| EXPECT_EQ(observed_changes_, changes_to_wait_for_); |
| } |
| |
| const net::CertVerifier::Config& verifier_config_during_change() const { |
| return verifier_config_during_change_; |
| } |
| |
| int observed_changes() const { return observed_changes_; } |
| |
| private: |
| int observed_changes_ = 0; |
| int changes_to_wait_for_ = 0; |
| net::CertVerifier::Config verifier_config_during_change_; |
| std::unique_ptr<base::RunLoop> run_loop_; |
| }; |
| |
| class NetworkServiceSSLConfigServiceTest : public testing::Test { |
| public: |
| NetworkServiceSSLConfigServiceTest() |
| : scoped_task_environment_( |
| base::test::ScopedTaskEnvironment::MainThreadType::IO), |
| network_service_(NetworkService::CreateForTesting()) {} |
| ~NetworkServiceSSLConfigServiceTest() override { |
| NetworkContext::SetCertVerifierForTesting(nullptr); |
| } |
| |
| // Creates a NetworkContext using the specified NetworkContextParams, and |
| // stores it in |network_context_|. |
| void SetUpNetworkContext( |
| mojom::NetworkContextParamsPtr network_context_params) { |
| network_context_params->ssl_config_client_request = |
| mojo::MakeRequest(&ssl_config_client_); |
| network_context_ = std::make_unique<NetworkContext>( |
| network_service_.get(), mojo::MakeRequest(&network_context_ptr_), |
| std::move(network_context_params)); |
| } |
| |
| // Returns the current SSLConfig for |network_context_|. |
| net::SSLConfig GetSSLConfig() { |
| net::SSLConfig ssl_config; |
| network_context_->url_request_context()->ssl_config_service()->GetSSLConfig( |
| &ssl_config); |
| return ssl_config; |
| } |
| |
| // Runs two conversion tests for |mojo_config|. Uses it as a initial |
| // SSLConfig for a NetworkContext, making sure it matches |
| // |expected_net_config|. Then switches to the default configuration and then |
| // back to |mojo_config|, to make sure it works as a new configuration. The |
| // expected configuration must not be the default configuration. |
| void RunConversionTests(const mojom::SSLConfig& mojo_config, |
| const net::SSLConfig& expected_net_config) { |
| // The expected configuration must not be the default configuration, or the |
| // change test won't send an event. |
| EXPECT_FALSE(net::SSLConfigService::SSLConfigsAreEqualForTesting( |
| net::SSLConfig(), expected_net_config)); |
| |
| // Set up |mojo_config| as the initial configuration of a NetworkContext. |
| mojom::NetworkContextParamsPtr network_context_params = |
| mojom::NetworkContextParams::New(); |
| network_context_params->initial_ssl_config = mojo_config.Clone(); |
| SetUpNetworkContext(std::move(network_context_params)); |
| EXPECT_TRUE(net::SSLConfigService::SSLConfigsAreEqualForTesting( |
| GetSSLConfig(), expected_net_config)); |
| // Sanity check. |
| EXPECT_FALSE(net::SSLConfigService::SSLConfigsAreEqualForTesting( |
| GetSSLConfig(), net::SSLConfig())); |
| |
| // Reset the configuration to the default ones, and check the results. |
| TestSSLConfigServiceObserver observer( |
| network_context_->url_request_context()->ssl_config_service()); |
| ssl_config_client_->OnSSLConfigUpdated(mojom::SSLConfig::New()); |
| observer.WaitForChange(); |
| EXPECT_TRUE(net::SSLConfigService::SSLConfigsAreEqualForTesting( |
| GetSSLConfig(), net::SSLConfig())); |
| EXPECT_TRUE(net::SSLConfigService::SSLConfigsAreEqualForTesting( |
| observer.ssl_config_during_change(), net::SSLConfig())); |
| // Sanity check. |
| EXPECT_FALSE(net::SSLConfigService::SSLConfigsAreEqualForTesting( |
| GetSSLConfig(), expected_net_config)); |
| |
| // Set the configuration to |mojo_config| again, and check the results. |
| ssl_config_client_->OnSSLConfigUpdated(mojo_config.Clone()); |
| observer.WaitForChange(); |
| EXPECT_TRUE(net::SSLConfigService::SSLConfigsAreEqualForTesting( |
| GetSSLConfig(), expected_net_config)); |
| EXPECT_TRUE(net::SSLConfigService::SSLConfigsAreEqualForTesting( |
| observer.ssl_config_during_change(), expected_net_config)); |
| } |
| |
| // Runs two conversion tests for |mojo_config|. Uses it as an initial |
| // net::CertVerifier::Config for a NetworkContext, making sure it matches |
| // |expected_net_config|. Then switches to the default configuration and then |
| // back to |mojo_config|, to make sure it works as a new configuration. The |
| // expected configuration must not be the default configuration. |
| void RunCertConversionTests( |
| const mojom::SSLConfig& mojo_config, |
| const net::CertVerifier::Config& expected_net_config) { |
| TestCertVerifierConfigObserver observer; |
| NetworkContext::SetCertVerifierForTesting(&observer); |
| |
| EXPECT_NE(net::CertVerifier::Config(), expected_net_config); |
| |
| // Set up |mojo_config| as the initial configuration of a NetworkContext. |
| mojom::NetworkContextParamsPtr network_context_params = |
| mojom::NetworkContextParams::New(); |
| network_context_params->initial_ssl_config = mojo_config.Clone(); |
| SetUpNetworkContext(std::move(network_context_params)); |
| |
| // Make sure the initial configuration is set. |
| observer.WaitForChange(); |
| EXPECT_EQ(observer.verifier_config_during_change(), expected_net_config); |
| // Sanity check. |
| EXPECT_NE(observer.verifier_config_during_change(), |
| net::CertVerifier::Config()); |
| |
| // Reset the configuration to the default ones, and check the results. |
| ssl_config_client_->OnSSLConfigUpdated(mojom::SSLConfig::New()); |
| observer.WaitForChange(); |
| EXPECT_EQ(observer.verifier_config_during_change(), |
| net::CertVerifier::Config()); |
| // Sanity check. |
| EXPECT_NE(observer.verifier_config_during_change(), expected_net_config); |
| |
| // Set the configuration to |mojo_config| again, and check the results. |
| ssl_config_client_->OnSSLConfigUpdated(mojo_config.Clone()); |
| observer.WaitForChange(); |
| EXPECT_EQ(observer.verifier_config_during_change(), expected_net_config); |
| |
| // Reset the CertVerifier for subsequent invocations. |
| NetworkContext::SetCertVerifierForTesting(nullptr); |
| } |
| |
| protected: |
| base::test::ScopedTaskEnvironment scoped_task_environment_; |
| std::unique_ptr<NetworkService> network_service_; |
| mojom::SSLConfigClientPtr ssl_config_client_; |
| mojom::NetworkContextPtr network_context_ptr_; |
| std::unique_ptr<NetworkContext> network_context_; |
| }; |
| |
| // Check that passing in a no mojom::SSLConfig matches the default |
| // net::SSLConfig. |
| TEST_F(NetworkServiceSSLConfigServiceTest, NoSSLConfig) { |
| SetUpNetworkContext(mojom::NetworkContextParams::New()); |
| EXPECT_TRUE(net::SSLConfigService::SSLConfigsAreEqualForTesting( |
| GetSSLConfig(), net::SSLConfig())); |
| |
| // Make sure the default TLS version range is as expected. |
| EXPECT_EQ(net::kDefaultSSLVersionMin, GetSSLConfig().version_min); |
| EXPECT_EQ(net::kDefaultSSLVersionMax, GetSSLConfig().version_max); |
| } |
| |
| // Check that passing in the default mojom::SSLConfig matches the default |
| // net::SSLConfig. |
| TEST_F(NetworkServiceSSLConfigServiceTest, Default) { |
| mojom::NetworkContextParamsPtr network_context_params = |
| mojom::NetworkContextParams::New(); |
| network_context_params->initial_ssl_config = mojom::SSLConfig::New(); |
| SetUpNetworkContext(std::move(network_context_params)); |
| EXPECT_TRUE(net::SSLConfigService::SSLConfigsAreEqualForTesting( |
| GetSSLConfig(), net::SSLConfig())); |
| |
| // Make sure the default TLS version range is as expected. |
| EXPECT_EQ(net::kDefaultSSLVersionMin, GetSSLConfig().version_min); |
| EXPECT_EQ(net::kDefaultSSLVersionMax, GetSSLConfig().version_max); |
| } |
| |
| // Check that passing in the default mojom::SSLConfig matches the default |
| // net::CertVerifier::Config. |
| TEST_F(NetworkServiceSSLConfigServiceTest, DefaultCertConfig) { |
| TestCertVerifierConfigObserver observer; |
| NetworkContext::SetCertVerifierForTesting(&observer); |
| |
| mojom::NetworkContextParamsPtr network_context_params = |
| mojom::NetworkContextParams::New(); |
| network_context_params->initial_ssl_config = mojom::SSLConfig::New(); |
| SetUpNetworkContext(std::move(network_context_params)); |
| |
| observer.WaitForChange(); |
| |
| net::CertVerifier::Config default_config; |
| EXPECT_EQ(observer.verifier_config_during_change(), default_config); |
| |
| NetworkContext::SetCertVerifierForTesting(nullptr); |
| } |
| |
| TEST_F(NetworkServiceSSLConfigServiceTest, RevCheckingEnabled) { |
| net::CertVerifier::Config expected_net_config; |
| // Use the opposite of the default value. |
| expected_net_config.enable_rev_checking = |
| !expected_net_config.enable_rev_checking; |
| |
| mojom::SSLConfigPtr mojo_config = mojom::SSLConfig::New(); |
| mojo_config->rev_checking_enabled = expected_net_config.enable_rev_checking; |
| |
| RunCertConversionTests(*mojo_config, expected_net_config); |
| } |
| |
| TEST_F(NetworkServiceSSLConfigServiceTest, |
| RevCheckingRequiredLocalTrustAnchors) { |
| net::CertVerifier::Config expected_net_config; |
| // Use the opposite of the default value. |
| expected_net_config.require_rev_checking_local_anchors = |
| !expected_net_config.require_rev_checking_local_anchors; |
| |
| mojom::SSLConfigPtr mojo_config = mojom::SSLConfig::New(); |
| mojo_config->rev_checking_required_local_anchors = |
| expected_net_config.require_rev_checking_local_anchors; |
| |
| RunCertConversionTests(*mojo_config, expected_net_config); |
| } |
| |
| TEST_F(NetworkServiceSSLConfigServiceTest, Sha1LocalAnchorsEnabled) { |
| net::CertVerifier::Config expected_net_config; |
| // Use the opposite of the default value. |
| expected_net_config.enable_sha1_local_anchors = |
| !expected_net_config.enable_sha1_local_anchors; |
| |
| mojom::SSLConfigPtr mojo_config = mojom::SSLConfig::New(); |
| mojo_config->sha1_local_anchors_enabled = |
| expected_net_config.enable_sha1_local_anchors; |
| |
| RunCertConversionTests(*mojo_config, expected_net_config); |
| } |
| |
| TEST_F(NetworkServiceSSLConfigServiceTest, SymantecEnforcementDisabled) { |
| net::CertVerifier::Config expected_net_config; |
| // Use the opposite of the default value. |
| expected_net_config.disable_symantec_enforcement = |
| !expected_net_config.disable_symantec_enforcement; |
| |
| mojom::SSLConfigPtr mojo_config = mojom::SSLConfig::New(); |
| mojo_config->symantec_enforcement_disabled = |
| expected_net_config.disable_symantec_enforcement; |
| |
| RunCertConversionTests(*mojo_config, expected_net_config); |
| } |
| |
| TEST_F(NetworkServiceSSLConfigServiceTest, SSLVersion) { |
| const struct { |
| mojom::SSLVersion mojo_ssl_version; |
| int net_ssl_version; |
| } kVersionTable[] = { |
| {mojom::SSLVersion::kTLS1, net::SSL_PROTOCOL_VERSION_TLS1}, |
| {mojom::SSLVersion::kTLS11, net::SSL_PROTOCOL_VERSION_TLS1_1}, |
| {mojom::SSLVersion::kTLS12, net::SSL_PROTOCOL_VERSION_TLS1_2}, |
| {mojom::SSLVersion::kTLS13, net::SSL_PROTOCOL_VERSION_TLS1_3}, |
| }; |
| |
| for (size_t min_index = 0; min_index < base::size(kVersionTable); |
| ++min_index) { |
| for (size_t max_index = min_index; max_index < base::size(kVersionTable); |
| ++max_index) { |
| // If the versions match the default values, skip this value in the table. |
| // The defaults will get plenty of testing anyways, when switching back to |
| // the default values in RunConversionTests(). |
| if (kVersionTable[min_index].net_ssl_version == |
| net::SSLConfig().version_min && |
| kVersionTable[max_index].net_ssl_version == |
| net::SSLConfig().version_max) { |
| continue; |
| } |
| net::SSLConfig expected_net_config; |
| expected_net_config.version_min = |
| kVersionTable[min_index].net_ssl_version; |
| expected_net_config.version_max = |
| kVersionTable[max_index].net_ssl_version; |
| |
| mojom::SSLConfigPtr mojo_config = mojom::SSLConfig::New(); |
| mojo_config->version_min = kVersionTable[min_index].mojo_ssl_version; |
| mojo_config->version_max = kVersionTable[max_index].mojo_ssl_version; |
| |
| RunConversionTests(*mojo_config, expected_net_config); |
| } |
| } |
| } |
| |
| TEST_F(NetworkServiceSSLConfigServiceTest, InitialConfigDisableCipherSuite) { |
| net::SSLConfig expected_net_config; |
| expected_net_config.disabled_cipher_suites.push_back(0x0004); |
| |
| mojom::SSLConfigPtr mojo_config = mojom::SSLConfig::New(); |
| mojo_config->disabled_cipher_suites = |
| expected_net_config.disabled_cipher_suites; |
| |
| RunConversionTests(*mojo_config, expected_net_config); |
| } |
| |
| TEST_F(NetworkServiceSSLConfigServiceTest, |
| InitialConfigDisableTwoCipherSuites) { |
| net::SSLConfig expected_net_config; |
| expected_net_config.disabled_cipher_suites.push_back(0x0004); |
| expected_net_config.disabled_cipher_suites.push_back(0x0005); |
| |
| mojom::SSLConfigPtr mojo_config = mojom::SSLConfig::New(); |
| mojo_config->disabled_cipher_suites = |
| expected_net_config.disabled_cipher_suites; |
| |
| RunConversionTests(*mojo_config, expected_net_config); |
| } |
| |
| TEST_F(NetworkServiceSSLConfigServiceTest, CanShareConnectionWithClientCerts) { |
| // Create a default NetworkContext and test that |
| // CanShareConnectionWithClientCerts returns false. |
| SetUpNetworkContext(mojom::NetworkContextParams::New()); |
| |
| net::SSLConfigService* config_service = |
| network_context_->url_request_context()->ssl_config_service(); |
| |
| EXPECT_FALSE( |
| config_service->CanShareConnectionWithClientCerts("example.com")); |
| EXPECT_FALSE( |
| config_service->CanShareConnectionWithClientCerts("example.net")); |
| |
| // Configure policy to allow example.com (but no subdomains), and example.net |
| // (including subdomains), update the config, and test that pooling is allowed |
| // with this policy. |
| mojom::SSLConfigPtr mojo_config = mojom::SSLConfig::New(); |
| mojo_config->client_cert_pooling_policy = {".example.com", "example.net"}; |
| |
| TestSSLConfigServiceObserver observer(config_service); |
| ssl_config_client_->OnSSLConfigUpdated(std::move(mojo_config)); |
| observer.WaitForChange(); |
| |
| EXPECT_TRUE(config_service->CanShareConnectionWithClientCerts("example.com")); |
| EXPECT_FALSE( |
| config_service->CanShareConnectionWithClientCerts("sub.example.com")); |
| |
| EXPECT_TRUE(config_service->CanShareConnectionWithClientCerts("example.net")); |
| EXPECT_TRUE( |
| config_service->CanShareConnectionWithClientCerts("sub.example.net")); |
| EXPECT_TRUE( |
| config_service->CanShareConnectionWithClientCerts("sub.sub.example.net")); |
| EXPECT_FALSE( |
| config_service->CanShareConnectionWithClientCerts("notexample.net")); |
| |
| EXPECT_FALSE( |
| config_service->CanShareConnectionWithClientCerts("example.org")); |
| |
| // Reset the configuration to the default and check that pooling is no longer |
| // allowed. |
| ssl_config_client_->OnSSLConfigUpdated(mojom::SSLConfig::New()); |
| observer.WaitForChange(); |
| |
| EXPECT_FALSE( |
| config_service->CanShareConnectionWithClientCerts("example.com")); |
| EXPECT_FALSE( |
| config_service->CanShareConnectionWithClientCerts("example.net")); |
| } |
| |
| #if !defined(OS_IOS) && !defined(OS_ANDROID) |
| TEST_F(NetworkServiceSSLConfigServiceTest, CRLSetIsApplied) { |
| SetUpNetworkContext(mojom::NetworkContextParams::New()); |
| |
| SSLConfigServiceMojo* config_service = static_cast<SSLConfigServiceMojo*>( |
| network_context_->url_request_context()->ssl_config_service()); |
| |
| scoped_refptr<net::X509Certificate> root_cert = |
| net::CreateCertificateChainFromFile( |
| net::GetTestCertsDirectory(), "root_ca_cert.pem", |
| net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE); |
| ASSERT_TRUE(root_cert); |
| net::ScopedTestRoot test_root(root_cert.get()); |
| |
| scoped_refptr<net::X509Certificate> cert = |
| net::CreateCertificateChainFromFile( |
| net::GetTestCertsDirectory(), "ok_cert.pem", |
| net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE); |
| ASSERT_TRUE(cert); |
| |
| // Ensure that |cert| is trusted without any CRLSet explicitly configured. |
| net::TestCompletionCallback callback1; |
| net::CertVerifyResult cert_verify_result1; |
| std::unique_ptr<net::CertVerifier::Request> request1; |
| int result = network_context_->url_request_context()->cert_verifier()->Verify( |
| net::CertVerifier::RequestParams(cert, "127.0.0.1", 0, std::string()), |
| &cert_verify_result1, callback1.callback(), &request1, |
| net::NetLogWithSource()); |
| ASSERT_THAT(callback1.GetResult(result), net::test::IsOk()); |
| |
| // Configure an explicit CRLSet that removes trust in |leaf_cert| by SPKI. |
| base::StringPiece spki; |
| ASSERT_TRUE(net::asn1::ExtractSPKIFromDERCert( |
| net::x509_util::CryptoBufferAsStringPiece(root_cert->cert_buffer()), |
| &spki)); |
| net::SHA256HashValue spki_sha256; |
| crypto::SHA256HashString(spki, spki_sha256.data, sizeof(spki_sha256.data)); |
| |
| config_service->OnNewCRLSet(net::CRLSet::ForTesting( |
| false, &spki_sha256, cert->serial_number(), "", {})); |
| |
| // Ensure that |cert| is revoked, due to the CRLSet being applied. |
| net::TestCompletionCallback callback2; |
| net::CertVerifyResult cert_verify_result2; |
| std::unique_ptr<net::CertVerifier::Request> request2; |
| result = network_context_->url_request_context()->cert_verifier()->Verify( |
| net::CertVerifier::RequestParams(cert, "127.0.0.1", 0, std::string()), |
| &cert_verify_result2, callback2.callback(), &request2, |
| net::NetLogWithSource()); |
| ASSERT_THAT(callback2.GetResult(result), |
| net::test::IsError(net::ERR_CERT_REVOKED)); |
| } |
| |
| #endif // !defined(OS_IOS) && !defined(OS_ANDROID) |
| |
| } // namespace |
| } // namespace network |