diff options
author | kmarshall <kmarshall@chromium.org> | 2016-03-02 13:41:39 -0800 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2016-03-02 21:43:41 +0000 |
commit | c80f5095f045ad1712f1f1075a44547a561f774a (patch) | |
tree | 9b23343cb2ad36e6e63ecc7793cb2f93bfd0b23e /blimp/client | |
parent | f0444bfd6b3c6b43f34da7709debdbc248395ef5 (diff) | |
download | chromium_src-c80f5095f045ad1712f1f1075a44547a561f774a.zip chromium_src-c80f5095f045ad1712f1f1075a44547a561f774a.tar.gz chromium_src-c80f5095f045ad1712f1f1075a44547a561f774a.tar.bz2 |
Blimp: add support for SSL connections.
This CL allows the Blimp client to establish TLS-protected channels with the backend engine. The authenticity of the engine is validated by checking if its cert is an exact match of a certificate provided separately by the Assigner API.
* Create new Blimp SSL transport class: SSLClientTransport.
* Create custom CertValidator for checking an exact cert match against the SSL peer's cert
* Integrate SSLClientTransport with BlimpClientSession.
* Assignment: add certificate field.
* AssignmentSource: add certificate file reading; PEM file parsing;
X509 certificate parsing.
* Created new DEPS entries as appropriate.
R=wez@chromium.org
CC=rsleevi@chromium.org
BUG=585279,589202
Review URL: https://codereview.chromium.org/1696563002
Cr-Commit-Position: refs/heads/master@{#378839}
Diffstat (limited to 'blimp/client')
-rw-r--r-- | blimp/client/BUILD.gn | 22 | ||||
-rw-r--r-- | blimp/client/DEPS | 3 | ||||
-rw-r--r-- | blimp/client/app/android/blimp_jni_registrar.cc | 2 | ||||
-rw-r--r-- | blimp/client/app/blimp_client_switches.cc | 12 | ||||
-rw-r--r-- | blimp/client/app/blimp_client_switches.h | 20 | ||||
-rw-r--r-- | blimp/client/session/assignment_source.cc | 239 | ||||
-rw-r--r-- | blimp/client/session/assignment_source.h | 57 | ||||
-rw-r--r-- | blimp/client/session/assignment_source_unittest.cc | 270 | ||||
-rw-r--r-- | blimp/client/session/blimp_client_session.cc | 22 | ||||
-rw-r--r-- | blimp/client/session/test_selfsigned_cert.pem | 11 |
10 files changed, 413 insertions, 245 deletions
diff --git a/blimp/client/BUILD.gn b/blimp/client/BUILD.gn index d2ddc2b..465adb7 100644 --- a/blimp/client/BUILD.gn +++ b/blimp/client/BUILD.gn @@ -27,6 +27,7 @@ source_set("blimp_client") { defines = [ "BLIMP_CLIENT_IMPLEMENTATION=1" ] public_deps = [ + "//components/safe_json", "//ui/events", ] @@ -49,12 +50,6 @@ source_set("blimp_client_unit_tests") { sources = [] - # TODO(dtrainor): Fix the test harness to allow this to run on Android. - # See crbug.com/588240. - if (is_linux) { - sources += [ "session/assignment_source_unittest.cc" ] - } - deps = [ ":blimp_client", "//base", @@ -63,6 +58,19 @@ source_set("blimp_client_unit_tests") { "//testing/gmock", "//testing/gtest", ] + + data = [] + + # TODO(dtrainor): Fix the test harness to allow this to run on Android. + # See crbug.com/588240. + if (is_linux) { + sources += [ "session/assignment_source_unittest.cc" ] + deps += [ + "//components/safe_json:test_support", + "//net:test_support", + ] + data += [ "session/test_selfsigned_cert.pem" ] + } } source_set("app_unit_tests") { @@ -342,6 +350,7 @@ if (is_android) { "//base", "//blimp/common/proto", "//blimp/net:blimp_net", + "//components/safe_json/android:safe_json_jni_headers", "//skia", "//ui/gfx/geometry", "//ui/gl", @@ -375,6 +384,7 @@ if (is_android) { ":blimp_java", ":blimp_java_resources", "//base:base_java", + "//components/safe_json/android:safe_json_java", "//net/android:net_java", google_play_services_resources, ] diff --git a/blimp/client/DEPS b/blimp/client/DEPS index 62e4870..e1fa29a 100644 --- a/blimp/client/DEPS +++ b/blimp/client/DEPS @@ -2,7 +2,8 @@ include_rules = [ "+base", "+cc", "-cc/blink", - "-chrome" + "-chrome", + "+components/safe_json", "-content", "+gpu", "+jni", diff --git a/blimp/client/app/android/blimp_jni_registrar.cc b/blimp/client/app/android/blimp_jni_registrar.cc index 0ad0bcb..2f6ad4d 100644 --- a/blimp/client/app/android/blimp_jni_registrar.cc +++ b/blimp/client/app/android/blimp_jni_registrar.cc @@ -10,6 +10,7 @@ #include "blimp/client/app/android/blimp_view.h" #include "blimp/client/app/android/tab_control_feature_android.h" #include "blimp/client/app/android/toolbar.h" +#include "components/safe_json/android/component_jni_registrar.h" namespace blimp { namespace client { @@ -20,6 +21,7 @@ base::android::RegistrationMethod kBlimpRegistrationMethods[] = { {"Toolbar", Toolbar::RegisterJni}, {"BlimpView", BlimpView::RegisterJni}, {"BlimpClientSessionAndroid", BlimpClientSessionAndroid::RegisterJni}, + {"SafeJson", safe_json::android::RegisterSafeJsonJni}, {"TabControlFeatureAndroid", TabControlFeatureAndroid::RegisterJni}, }; diff --git a/blimp/client/app/blimp_client_switches.cc b/blimp/client/app/blimp_client_switches.cc index cfcf2a0..da7dcda 100644 --- a/blimp/client/app/blimp_client_switches.cc +++ b/blimp/client/app/blimp_client_switches.cc @@ -7,11 +7,13 @@ namespace blimp { namespace switches { -// Specifies the blimplet scheme, IP-address and port to connect to, e.g.: -// --blimplet-host="tcp:127.0.0.1:25467". Valid schemes are "ssl", -// "tcp", and "quic". -// TODO(nyquist): Add support for DNS-lookup. See http://crbug.com/576857. -const char kBlimpletEndpoint[] = "blimplet-endpoint"; +const char kEngineCertPath[] = "engine-cert-path"; + +const char kEngineIP[] = "engine-ip"; + +const char kEnginePort[] = "engine-port"; + +const char kEngineTransport[] = "engine-transport"; } // namespace switches } // namespace blimp diff --git a/blimp/client/app/blimp_client_switches.h b/blimp/client/app/blimp_client_switches.h index 19b91dd..b06958f 100644 --- a/blimp/client/app/blimp_client_switches.h +++ b/blimp/client/app/blimp_client_switches.h @@ -10,7 +10,25 @@ namespace blimp { namespace switches { -extern const char kBlimpletEndpoint[]; +// The path to the engine's PEM-encoded X509 certificate. +// If specified, SSL connected Engines must supply this certificate +// for the connection to be valid. +// e.g.: +// --engine-cert-path=/home/blimp/certs/cert.pem +extern const char kEngineCertPath[]; + +// Specifies the engine's IP address. Must be used in conjunction with +// --engine-port and --engine-transport. +extern const char kEngineIP[]; + +// Specifies the engine's listening port (1-65535). +// Must be used in conjunction with --engine-ip and --engine-transport. +extern const char kEnginePort[]; + +// Specifies the transport used to communicate with the engine. +// Can be "tcp" or "ssl". +// Must be used in conjunction with --engine-ip and --engine-port. +extern const char kEngineTransport[]; } // namespace switches } // namespace blimp diff --git a/blimp/client/session/assignment_source.cc b/blimp/client/session/assignment_source.cc index 242d783..bbd8d1a 100644 --- a/blimp/client/session/assignment_source.cc +++ b/blimp/client/session/assignment_source.cc @@ -7,19 +7,22 @@ #include "base/bind.h" #include "base/callback_helpers.h" #include "base/command_line.h" +#include "base/files/file_util.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" #include "base/location.h" +#include "base/memory/ref_counted.h" #include "base/numerics/safe_conversions.h" #include "base/strings/string_number_conversions.h" +#include "base/task_runner_util.h" #include "base/values.h" #include "blimp/client/app/blimp_client_switches.h" #include "blimp/common/protocol_version.h" +#include "components/safe_json/safe_json_parser.h" #include "net/base/ip_address.h" #include "net/base/ip_endpoint.h" #include "net/base/load_flags.h" #include "net/base/net_errors.h" -#include "net/base/url_util.h" #include "net/http/http_status_code.h" #include "net/proxy/proxy_config_service.h" #include "net/proxy/proxy_service.h" @@ -40,55 +43,11 @@ const char kProtocolVersionKey[] = "protocol_version"; const char kClientTokenKey[] = "clientToken"; const char kHostKey[] = "host"; const char kPortKey[] = "port"; -const char kCertificateFingerprintKey[] = "certificateFingerprint"; const char kCertificateKey[] = "certificate"; -// URL scheme constants for custom assignments. See the '--blimplet-endpoint' -// documentation in blimp_client_switches.cc for details. -const char kCustomSSLScheme[] = "ssl"; -const char kCustomTCPScheme[] = "tcp"; -const char kCustomQUICScheme[] = "quic"; - -Assignment GetCustomBlimpletAssignment() { - GURL url(base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( - switches::kBlimpletEndpoint)); - - std::string host; - int port; - if (url.is_empty() || !url.is_valid() || !url.has_scheme() || - !net::ParseHostAndPort(url.path(), &host, &port)) { - return Assignment(); - } - - net::IPAddress ip_address; - if (!ip_address.AssignFromIPLiteral(host)) { - CHECK(false) << "Invalid BlimpletAssignment host " << host; - } - - if (!base::IsValueInRangeForNumericType<uint16_t>(port)) { - CHECK(false) << "Invalid BlimpletAssignment port " << port; - } - - Assignment::TransportProtocol protocol = - Assignment::TransportProtocol::UNKNOWN; - if (url.has_scheme()) { - if (url.SchemeIs(kCustomSSLScheme)) { - protocol = Assignment::TransportProtocol::SSL; - } else if (url.SchemeIs(kCustomTCPScheme)) { - protocol = Assignment::TransportProtocol::TCP; - } else if (url.SchemeIs(kCustomQUICScheme)) { - protocol = Assignment::TransportProtocol::QUIC; - } else { - CHECK(false) << "Invalid BlimpletAssignment scheme " << url.scheme(); - } - } - - Assignment assignment; - assignment.transport_protocol = protocol; - assignment.ip_endpoint = net::IPEndPoint(ip_address, port); - assignment.client_token = kDummyClientToken; - return assignment; -} +// Possible arguments for the "--engine-transport" command line parameter. +const char kSSLTransportValue[] = "ssl"; +const char kTCPTransportValue[] = "tcp"; GURL GetBlimpAssignerURL() { // TODO(dtrainor): Add a way to specify another assigner. @@ -98,8 +57,8 @@ GURL GetBlimpAssignerURL() { class SimpleURLRequestContextGetter : public net::URLRequestContextGetter { public: SimpleURLRequestContextGetter( - const scoped_refptr<base::SingleThreadTaskRunner>& io_loop_task_runner) - : io_loop_task_runner_(io_loop_task_runner), + scoped_refptr<base::SingleThreadTaskRunner> io_loop_task_runner) + : io_loop_task_runner_(std::move(io_loop_task_runner)), proxy_config_service_(net::ProxyService::CreateSystemProxyConfigService( io_loop_task_runner_, io_loop_task_runner_)) {} @@ -136,45 +95,142 @@ class SimpleURLRequestContextGetter : public net::URLRequestContextGetter { DISALLOW_COPY_AND_ASSIGN(SimpleURLRequestContextGetter); }; +bool IsValidIpPortNumber(unsigned port) { + return port > 0 && port <= 65535; +} + +// Populates an Assignment using command-line parameters, if provided. +// Returns a null Assignment if no parameters were set. +// Must be called on a thread suitable for file IO. +Assignment GetAssignmentFromCommandLine() { + Assignment assignment; + assignment.client_token = kDummyClientToken; + + unsigned port_parsed = 0; + if (!base::StringToUint( + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + switches::kEnginePort), + &port_parsed) || + !IsValidIpPortNumber(port_parsed)) { + DLOG(FATAL) << "--engine-port must be a value between 1 and 65535."; + return Assignment(); + } + + net::IPAddress ip_address; + std::string ip_str = + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + switches::kEngineIP); + if (!ip_address.AssignFromIPLiteral(ip_str)) { + DLOG(FATAL) << "Invalid engine IP " << ip_str; + return Assignment(); + } + assignment.engine_endpoint = + net::IPEndPoint(ip_address, base::checked_cast<uint16_t>(port_parsed)); + + std::string transport_str = + base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII( + switches::kEngineTransport); + if (transport_str == kSSLTransportValue) { + assignment.transport_protocol = Assignment::TransportProtocol::SSL; + } else if (transport_str == kTCPTransportValue) { + assignment.transport_protocol = Assignment::TransportProtocol::TCP; + } else { + DLOG(FATAL) << "Invalid engine transport " << transport_str; + return Assignment(); + } + + scoped_refptr<net::X509Certificate> cert; + if (assignment.transport_protocol == Assignment::TransportProtocol::SSL) { + base::FilePath cert_path = + base::CommandLine::ForCurrentProcess()->GetSwitchValuePath( + switches::kEngineCertPath); + if (cert_path.empty()) { + DLOG(FATAL) << "Missing required parameter --" + << switches::kEngineCertPath << "."; + return Assignment(); + } + std::string cert_str; + if (!base::ReadFileToString(cert_path, &cert_str)) { + DLOG(FATAL) << "Couldn't read from file: " + << cert_path.LossyDisplayName(); + return Assignment(); + } + net::CertificateList cert_list = + net::X509Certificate::CreateCertificateListFromBytes( + cert_str.data(), cert_str.size(), + net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE); + DLOG_IF(FATAL, (cert_list.size() != 1u)) + << "Only one cert is allowed in PEM cert list."; + assignment.cert = std::move(cert_list[0]); + } + + if (!assignment.IsValid()) { + DLOG(FATAL) << "Invalid command-line assignment."; + return Assignment(); + } + + return assignment; +} + } // namespace Assignment::Assignment() : transport_protocol(TransportProtocol::UNKNOWN) {} Assignment::~Assignment() {} -bool Assignment::is_null() const { - return ip_endpoint.address().empty() || ip_endpoint.port() == 0 || - transport_protocol == TransportProtocol::UNKNOWN; +bool Assignment::IsValid() const { + if (engine_endpoint.address().empty() || engine_endpoint.port() == 0 || + transport_protocol == TransportProtocol::UNKNOWN) { + return false; + } + if (transport_protocol == TransportProtocol::SSL && !cert) { + return false; + } + return true; } AssignmentSource::AssignmentSource( - const scoped_refptr<base::SingleThreadTaskRunner>& main_task_runner, - const scoped_refptr<base::SingleThreadTaskRunner>& io_task_runner) - : main_task_runner_(main_task_runner), - url_request_context_(new SimpleURLRequestContextGetter(io_task_runner)) {} + const scoped_refptr<base::SingleThreadTaskRunner>& network_task_runner, + const scoped_refptr<base::SingleThreadTaskRunner>& file_task_runner) + : file_task_runner_(std::move(file_task_runner)), + url_request_context_( + new SimpleURLRequestContextGetter(network_task_runner)), + weak_factory_(this) {} AssignmentSource::~AssignmentSource() {} void AssignmentSource::GetAssignment(const std::string& client_auth_token, const AssignmentCallback& callback) { - DCHECK(main_task_runner_->BelongsToCurrentThread()); + DCHECK(callback_.is_null()); + callback_ = AssignmentCallback(callback); - // Cancel any outstanding callback. - if (!callback_.is_null()) { - base::ResetAndReturn(&callback_) - .Run(AssignmentSource::Result::RESULT_SERVER_INTERRUPTED, Assignment()); + if (base::CommandLine::ForCurrentProcess()->HasSwitch(switches::kEngineIP)) { + base::PostTaskAndReplyWithResult( + file_task_runner_.get(), FROM_HERE, + base::Bind(&GetAssignmentFromCommandLine), + base::Bind(&AssignmentSource::OnGetAssignmentFromCommandLineDone, + weak_factory_.GetWeakPtr(), client_auth_token)); + } else { + QueryAssigner(client_auth_token); } - callback_ = AssignmentCallback(callback); +} - Assignment assignment = GetCustomBlimpletAssignment(); - if (!assignment.is_null()) { - // Post the result so that the behavior of this function is consistent. - main_task_runner_->PostTask( - FROM_HERE, base::Bind(base::ResetAndReturn(&callback_), - AssignmentSource::Result::RESULT_OK, assignment)); +void AssignmentSource::OnGetAssignmentFromCommandLineDone( + const std::string& client_auth_token, + Assignment parsed_assignment) { + // If GetAssignmentFromCommandLine succeeded, then return its output. + if (parsed_assignment.IsValid()) { + base::ResetAndReturn(&callback_) + .Run(AssignmentSource::RESULT_OK, parsed_assignment); return; } + // If no assignment was passed via the command line, then fall back on + // querying the Assigner service. + QueryAssigner(client_auth_token); +} + +void AssignmentSource::QueryAssigner(const std::string& client_auth_token) { // Call out to the network for a real assignment. Build the network request // to hit the assigner. url_fetcher_ = net::URLFetcher::Create(GetBlimpAssignerURL(), @@ -191,12 +247,10 @@ void AssignmentSource::GetAssignment(const std::string& client_auth_token, std::string json; base::JSONWriter::Write(dictionary, &json); url_fetcher_->SetUploadData("application/json", json); - url_fetcher_->Start(); } void AssignmentSource::OnURLFetchComplete(const net::URLFetcher* source) { - DCHECK(main_task_runner_->BelongsToCurrentThread()); DCHECK(!callback_.is_null()); DCHECK_EQ(url_fetcher_.get(), source); @@ -253,14 +307,14 @@ void AssignmentSource::ParseAssignerResponse() { return; } - // Attempt to interpret the response as JSON and treat it as a dictionary. - scoped_ptr<base::Value> json = base::JSONReader::Read(response); - if (!json) { - base::ResetAndReturn(&callback_) - .Run(AssignmentSource::Result::RESULT_BAD_RESPONSE, Assignment()); - return; - } + safe_json::SafeJsonParser::Parse( + response, + base::Bind(&AssignmentSource::OnJsonParsed, weak_factory_.GetWeakPtr()), + base::Bind(&AssignmentSource::OnJsonParseError, + weak_factory_.GetWeakPtr())); +} +void AssignmentSource::OnJsonParsed(scoped_ptr<base::Value> json) { const base::DictionaryValue* dict; if (!json->GetAsDictionary(&dict)) { base::ResetAndReturn(&callback_) @@ -272,12 +326,10 @@ void AssignmentSource::ParseAssignerResponse() { std::string client_token; std::string host; int port; - std::string cert_fingerprint; - std::string cert; + std::string cert_str; if (!(dict->GetString(kClientTokenKey, &client_token) && dict->GetString(kHostKey, &host) && dict->GetInteger(kPortKey, &port) && - dict->GetString(kCertificateFingerprintKey, &cert_fingerprint) && - dict->GetString(kCertificateKey, &cert))) { + dict->GetString(kCertificateKey, &cert_str))) { base::ResetAndReturn(&callback_) .Run(AssignmentSource::Result::RESULT_BAD_RESPONSE, Assignment()); return; @@ -296,18 +348,33 @@ void AssignmentSource::ParseAssignerResponse() { return; } - Assignment assignment; + net::CertificateList cert_list = + net::X509Certificate::CreateCertificateListFromBytes( + cert_str.data(), cert_str.size(), + net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE); + if (cert_list.size() != 1) { + base::ResetAndReturn(&callback_) + .Run(AssignmentSource::Result::RESULT_INVALID_CERT, Assignment()); + return; + } + // The assigner assumes SSL-only and all engines it assigns only communicate // over SSL. + Assignment assignment; assignment.transport_protocol = Assignment::TransportProtocol::SSL; - assignment.ip_endpoint = net::IPEndPoint(ip_address, port); + assignment.engine_endpoint = net::IPEndPoint(ip_address, port); assignment.client_token = client_token; - assignment.certificate = cert; - assignment.certificate_fingerprint = cert_fingerprint; + assignment.cert = std::move(cert_list[0]); base::ResetAndReturn(&callback_) .Run(AssignmentSource::Result::RESULT_OK, assignment); } +void AssignmentSource::OnJsonParseError(const std::string& error) { + DLOG(ERROR) << "Error while parsing assigner JSON: " << error; + base::ResetAndReturn(&callback_) + .Run(AssignmentSource::Result::RESULT_BAD_RESPONSE, Assignment()); +} + } // namespace client } // namespace blimp diff --git a/blimp/client/session/assignment_source.h b/blimp/client/session/assignment_source.h index dabe72c..bafd021 100644 --- a/blimp/client/session/assignment_source.h +++ b/blimp/client/session/assignment_source.h @@ -8,17 +8,21 @@ #include <string> #include "base/callback.h" +#include "base/memory/weak_ptr.h" #include "blimp/client/blimp_client_export.h" #include "net/base/ip_endpoint.h" #include "net/url_request/url_fetcher_delegate.h" namespace base { +class FilePath; class SingleThreadTaskRunner; +class Value; } namespace net { class URLFetcher; class URLRequestContextGetter; +class X509Certificate; } namespace blimp { @@ -38,21 +42,26 @@ struct BLIMP_CLIENT_EXPORT Assignment { UNKNOWN = 0, SSL = 1, TCP = 2, - QUIC = 3, }; Assignment(); ~Assignment(); + // Returns true if the net::IPEndPoint has an unspecified IP, port, or + // transport protocol. + bool IsValid() const; + + // Specifies the transport to use to connect to the engine. TransportProtocol transport_protocol; - net::IPEndPoint ip_endpoint; + + // Specifies the IP address and port on which to reach the engine. + net::IPEndPoint engine_endpoint; + + // Used to authenticate to the specified engine. std::string client_token; - std::string certificate; - std::string certificate_fingerprint; - // Returns true if the net::IPEndPoint has an unspecified IP, port, or - // transport protocol. - bool is_null() const; + // Specifies the X.509 certificate that the engine must report. + scoped_refptr<net::X509Certificate> cert; }; // AssignmentSource provides functionality to find out how a client should @@ -72,18 +81,21 @@ class BLIMP_CLIENT_EXPORT AssignmentSource : public net::URLFetcherDelegate { RESULT_OUT_OF_VMS = 7, RESULT_SERVER_ERROR = 8, RESULT_SERVER_INTERRUPTED = 9, - RESULT_NETWORK_FAILURE = 10 + RESULT_NETWORK_FAILURE = 10, + RESULT_INVALID_CERT = 11, }; typedef base::Callback<void(AssignmentSource::Result, const Assignment&)> AssignmentCallback; - // The |main_task_runner| should be the task runner for the UI thread because - // this will in some cases be used to trigger user interaction on the UI - // thread. + // |network_task_runner|: The task runner to use for querying the Assigner API + // over the network. + // |file_task_runner|: The task runner to use for reading cert files from disk + // (specified on the command line.) AssignmentSource( - const scoped_refptr<base::SingleThreadTaskRunner>& main_task_runner, - const scoped_refptr<base::SingleThreadTaskRunner>& io_task_runner); + const scoped_refptr<base::SingleThreadTaskRunner>& network_task_runner, + const scoped_refptr<base::SingleThreadTaskRunner>& file_task_runner); + ~AssignmentSource() override; // Retrieves a valid assignment for the client and posts the result to the @@ -94,20 +106,25 @@ class BLIMP_CLIENT_EXPORT AssignmentSource : public net::URLFetcherDelegate { void GetAssignment(const std::string& client_auth_token, const AssignmentCallback& callback); - // net::URLFetcherDelegate implementation: - void OnURLFetchComplete(const net::URLFetcher* source) override; - private: + void OnGetAssignmentFromCommandLineDone(const std::string& client_auth_token, + Assignment parsed_assignment); + void QueryAssigner(const std::string& client_auth_token); void ParseAssignerResponse(); + void OnJsonParsed(scoped_ptr<base::Value> json); + void OnJsonParseError(const std::string& error); + + // net::URLFetcherDelegate implementation: + void OnURLFetchComplete(const net::URLFetcher* source) override; - scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_; + // GetAssignment() callback, invoked after URLFetcher completion. + AssignmentCallback callback_; + scoped_refptr<base::SingleThreadTaskRunner> file_task_runner_; scoped_refptr<net::URLRequestContextGetter> url_request_context_; scoped_ptr<net::URLFetcher> url_fetcher_; - // This callback is set during a call to GetAssignment() and is cleared after - // the request has completed (whether it be a success or failure). - AssignmentCallback callback_; + base::WeakPtrFactory<AssignmentSource> weak_factory_; DISALLOW_COPY_AND_ASSIGN(AssignmentSource); }; diff --git a/blimp/client/session/assignment_source_unittest.cc b/blimp/client/session/assignment_source_unittest.cc index d893ae3..54f3e44 100644 --- a/blimp/client/session/assignment_source_unittest.cc +++ b/blimp/client/session/assignment_source_unittest.cc @@ -5,68 +5,80 @@ #include "blimp/client/session/assignment_source.h" #include "base/command_line.h" +#include "base/files/file_path.h" +#include "base/files/file_util.h" #include "base/json/json_reader.h" #include "base/json/json_writer.h" +#include "base/message_loop/message_loop.h" +#include "base/path_service.h" +#include "base/run_loop.h" #include "base/test/test_simple_task_runner.h" #include "base/thread_task_runner_handle.h" #include "base/values.h" #include "blimp/client/app/blimp_client_switches.h" #include "blimp/common/protocol_version.h" +#include "components/safe_json/testing_json_parser.h" +#include "net/base/test_data_directory.h" #include "net/url_request/test_url_fetcher_factory.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" using testing::_; +using testing::DoAll; using testing::InSequence; +using testing::NotNull; +using testing::Return; +using testing::SetArgPointee; namespace blimp { namespace client { namespace { +const uint8_t kTestIpAddress[] = {127, 0, 0, 1}; +const uint16_t kTestPort = 8086; +const char kTestIpAddressString[] = "127.0.0.1"; +const char kTcpTransportName[] = "tcp"; +const char kSslTransportName[] = "ssl"; +const char kCertRelativePath[] = + "blimp/client/session/test_selfsigned_cert.pem"; +const char kTestClientToken[] = "secrett0ken"; +const char kTestAuthToken[] = "UserAuthT0kenz"; + MATCHER_P(AssignmentEquals, assignment, "") { return arg.transport_protocol == assignment.transport_protocol && - arg.ip_endpoint == assignment.ip_endpoint && + arg.engine_endpoint == assignment.engine_endpoint && arg.client_token == assignment.client_token && - arg.certificate == assignment.certificate && - arg.certificate_fingerprint == assignment.certificate_fingerprint; -} - -net::IPEndPoint BuildIPEndPoint(const std::string& ip, int port) { - net::IPAddress ip_address; - EXPECT_TRUE(ip_address.AssignFromIPLiteral(ip)); - - return net::IPEndPoint(ip_address, port); -} - -Assignment BuildValidAssignment() { - Assignment assignment; - assignment.transport_protocol = Assignment::TransportProtocol::SSL; - assignment.ip_endpoint = BuildIPEndPoint("100.150.200.250", 500); - assignment.client_token = "SecretT0kenz"; - assignment.certificate_fingerprint = "WhaleWhaleWhale"; - assignment.certificate = "whaaaaaaaaaaaaale"; - return assignment; + ((!assignment.cert && !arg.cert) || + (arg.cert && assignment.cert && + arg.cert->Equals(assignment.cert.get()))); } -std::string BuildResponseFromAssignment(const Assignment& assignment) { - base::DictionaryValue dict; - dict.SetString("clientToken", assignment.client_token); - dict.SetString("host", assignment.ip_endpoint.address().ToString()); - dict.SetInteger("port", assignment.ip_endpoint.port()); - dict.SetString("certificateFingerprint", assignment.certificate_fingerprint); - dict.SetString("certificate", assignment.certificate); - +// Converts |value| to a JSON string. +std::string ValueToString(const base::Value& value) { std::string json; - base::JSONWriter::Write(dict, &json); + base::JSONWriter::Write(value, &json); return json; } class AssignmentSourceTest : public testing::Test { public: AssignmentSourceTest() - : task_runner_(new base::TestSimpleTaskRunner), - task_runner_handle_(task_runner_), - source_(task_runner_, task_runner_) {} + : source_(message_loop_.task_runner(), message_loop_.task_runner()) {} + + void SetUp() override { + base::FilePath src_root; + PathService::Get(base::DIR_SOURCE_ROOT, &src_root); + ASSERT_FALSE(src_root.empty()); + cert_path_ = src_root.Append(kCertRelativePath); + ASSERT_TRUE(base::ReadFileToString(cert_path_, &cert_pem_)); + net::CertificateList cert_list = + net::X509Certificate::CreateCertificateListFromBytes( + cert_pem_.data(), cert_pem_.size(), + net::X509Certificate::FORMAT_PEM_CERT_SEQUENCE); + ASSERT_FALSE(cert_list.empty()); + cert_ = std::move(cert_list[0]); + ASSERT_TRUE(cert_); + } // This expects the AssignmentSource::GetAssignment to return a custom // endpoint without having to hit the network. This will typically be used @@ -77,7 +89,7 @@ class AssignmentSourceTest : public testing::Test { base::Bind(&AssignmentSourceTest::AssignmentResponse, base::Unretained(this))); EXPECT_EQ(nullptr, factory_.GetFetcherByID(0)); - task_runner_->RunUntilIdle(); + base::RunLoop().RunUntilIdle(); } // See net/base/net_errors.h for possible status errors. @@ -90,11 +102,10 @@ class AssignmentSourceTest : public testing::Test { source_.GetAssignment(client_auth_token, base::Bind(&AssignmentSourceTest::AssignmentResponse, base::Unretained(this))); + base::RunLoop().RunUntilIdle(); net::TestURLFetcher* fetcher = factory_.GetFetcherByID(0); - task_runner_->RunUntilIdle(); - EXPECT_NE(nullptr, fetcher); EXPECT_EQ(kDefaultAssignerURL, fetcher->GetOriginalURL().spec()); @@ -125,30 +136,74 @@ class AssignmentSourceTest : public testing::Test { fetcher->SetResponseString(response); fetcher->delegate()->OnURLFetchComplete(fetcher); - task_runner_->RunUntilIdle(); + base::RunLoop().RunUntilIdle(); } MOCK_METHOD2(AssignmentResponse, void(AssignmentSource::Result, const Assignment&)); protected: + Assignment BuildSslAssignment(); + + // Builds simulated JSON response from the Assigner service. + scoped_ptr<base::DictionaryValue> BuildAssignerResponse(); + // Used to drive all AssignmentSource tasks. - scoped_refptr<base::TestSimpleTaskRunner> task_runner_; - base::ThreadTaskRunnerHandle task_runner_handle_; + // MessageLoop is required by TestingJsonParser's self-deletion logic. + // TODO(bauerb): Replace this with a TestSimpleTaskRunner once + // TestingJsonParser no longer requires having a MessageLoop. + base::MessageLoop message_loop_; net::TestURLFetcherFactory factory_; + // Path to the PEM-encoded certificate chain. + base::FilePath cert_path_; + + // Payload of PEM certificate chain at |cert_path_|. + std::string cert_pem_; + + // X509 certificate decoded from |cert_path_|. + scoped_refptr<net::X509Certificate> cert_; + AssignmentSource source_; + + // Allows safe_json to parse JSON in-process, instead of depending on a + // utility proces. + safe_json::TestingJsonParser::ScopedFactoryOverride json_parsing_factory_; }; +Assignment AssignmentSourceTest::BuildSslAssignment() { + Assignment assignment; + assignment.transport_protocol = Assignment::TransportProtocol::SSL; + assignment.engine_endpoint = net::IPEndPoint(kTestIpAddress, kTestPort); + assignment.client_token = kTestClientToken; + assignment.cert = cert_; + return assignment; +} + +scoped_ptr<base::DictionaryValue> +AssignmentSourceTest::BuildAssignerResponse() { + scoped_ptr<base::DictionaryValue> dict(new base::DictionaryValue); + dict->SetString("clientToken", kTestClientToken); + dict->SetString("host", kTestIpAddressString); + dict->SetInteger("port", kTestPort); + dict->SetString("certificate", cert_pem_); + return dict; +} + TEST_F(AssignmentSourceTest, TestTCPAlternateEndpointSuccess) { Assignment assignment; assignment.transport_protocol = Assignment::TransportProtocol::TCP; - assignment.ip_endpoint = BuildIPEndPoint("100.150.200.250", 500); + assignment.engine_endpoint = net::IPEndPoint(kTestIpAddress, kTestPort); assignment.client_token = kDummyClientToken; + assignment.cert = scoped_refptr<net::X509Certificate>(nullptr); base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( - switches::kBlimpletEndpoint, "tcp:100.150.200.250:500"); + switches::kEngineIP, kTestIpAddressString); + base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( + switches::kEnginePort, std::to_string(kTestPort)); + base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( + switches::kEngineTransport, kTcpTransportName); EXPECT_CALL(*this, AssignmentResponse(AssignmentSource::Result::RESULT_OK, AssignmentEquals(assignment))) @@ -160,27 +215,18 @@ TEST_F(AssignmentSourceTest, TestTCPAlternateEndpointSuccess) { TEST_F(AssignmentSourceTest, TestSSLAlternateEndpointSuccess) { Assignment assignment; assignment.transport_protocol = Assignment::TransportProtocol::SSL; - assignment.ip_endpoint = BuildIPEndPoint("100.150.200.250", 500); + assignment.engine_endpoint = net::IPEndPoint(kTestIpAddress, kTestPort); assignment.client_token = kDummyClientToken; + assignment.cert = cert_; base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( - switches::kBlimpletEndpoint, "ssl:100.150.200.250:500"); - - EXPECT_CALL(*this, AssignmentResponse(AssignmentSource::Result::RESULT_OK, - AssignmentEquals(assignment))) - .Times(1); - - GetAlternateAssignment(); -} - -TEST_F(AssignmentSourceTest, TestQUICAlternateEndpointSuccess) { - Assignment assignment; - assignment.transport_protocol = Assignment::TransportProtocol::QUIC; - assignment.ip_endpoint = BuildIPEndPoint("100.150.200.250", 500); - assignment.client_token = kDummyClientToken; - + switches::kEngineIP, kTestIpAddressString); + base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( + switches::kEnginePort, std::to_string(kTestPort)); base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( - switches::kBlimpletEndpoint, "quic:100.150.200.250:500"); + switches::kEngineTransport, kSslTransportName); + base::CommandLine::ForCurrentProcess()->AppendSwitchASCII( + switches::kEngineCertPath, cert_path_.value()); EXPECT_CALL(*this, AssignmentResponse(AssignmentSource::Result::RESULT_OK, AssignmentEquals(assignment))) @@ -190,44 +236,20 @@ TEST_F(AssignmentSourceTest, TestQUICAlternateEndpointSuccess) { } TEST_F(AssignmentSourceTest, TestSuccess) { - Assignment assignment = BuildValidAssignment(); + Assignment assignment = BuildSslAssignment(); EXPECT_CALL(*this, AssignmentResponse(AssignmentSource::Result::RESULT_OK, AssignmentEquals(assignment))) .Times(1); GetNetworkAssignmentAndWaitForResponse( - net::HTTP_OK, net::Error::OK, BuildResponseFromAssignment(assignment), - "UserAuthT0kenz", kEngineVersion); -} - -TEST_F(AssignmentSourceTest, TestSecondRequestInterruptsFirst) { - InSequence sequence; - Assignment assignment = BuildValidAssignment(); - - source_.GetAssignment("", - base::Bind(&AssignmentSourceTest::AssignmentResponse, - base::Unretained(this))); - - EXPECT_CALL(*this, AssignmentResponse( - AssignmentSource::Result::RESULT_SERVER_INTERRUPTED, - AssignmentEquals(Assignment()))) - .Times(1) - .RetiresOnSaturation(); - - EXPECT_CALL(*this, AssignmentResponse(AssignmentSource::Result::RESULT_OK, - AssignmentEquals(assignment))) - .Times(1) - .RetiresOnSaturation(); - - GetNetworkAssignmentAndWaitForResponse( - net::HTTP_OK, net::Error::OK, BuildResponseFromAssignment(assignment), - "UserAuthT0kenz", kEngineVersion); + net::HTTP_OK, net::Error::OK, ValueToString(*BuildAssignerResponse()), + kTestAuthToken, kEngineVersion); } TEST_F(AssignmentSourceTest, TestValidAfterError) { InSequence sequence; - Assignment assignment = BuildValidAssignment(); + Assignment assignment = BuildSslAssignment(); EXPECT_CALL(*this, AssignmentResponse( AssignmentSource::Result::RESULT_NETWORK_FAILURE, _)) @@ -241,11 +263,11 @@ TEST_F(AssignmentSourceTest, TestValidAfterError) { GetNetworkAssignmentAndWaitForResponse(net::HTTP_OK, net::Error::ERR_INSUFFICIENT_RESOURCES, - "", "UserAuthT0kenz", kEngineVersion); + "", kTestAuthToken, kEngineVersion); GetNetworkAssignmentAndWaitForResponse( - net::HTTP_OK, net::Error::OK, BuildResponseFromAssignment(assignment), - "UserAuthT0kenz", kEngineVersion); + net::HTTP_OK, net::Error::OK, ValueToString(*BuildAssignerResponse()), + kTestAuthToken, kEngineVersion); } TEST_F(AssignmentSourceTest, TestNetworkFailure) { @@ -253,14 +275,14 @@ TEST_F(AssignmentSourceTest, TestNetworkFailure) { AssignmentSource::Result::RESULT_NETWORK_FAILURE, _)); GetNetworkAssignmentAndWaitForResponse(net::HTTP_OK, net::Error::ERR_INSUFFICIENT_RESOURCES, - "", "UserAuthT0kenz", kEngineVersion); + "", kTestAuthToken, kEngineVersion); } TEST_F(AssignmentSourceTest, TestBadRequest) { EXPECT_CALL(*this, AssignmentResponse( AssignmentSource::Result::RESULT_BAD_REQUEST, _)); GetNetworkAssignmentAndWaitForResponse(net::HTTP_BAD_REQUEST, net::Error::OK, - "", "UserAuthT0kenz", kEngineVersion); + "", kTestAuthToken, kEngineVersion); } TEST_F(AssignmentSourceTest, TestUnauthorized) { @@ -268,21 +290,21 @@ TEST_F(AssignmentSourceTest, TestUnauthorized) { AssignmentResponse( AssignmentSource::Result::RESULT_EXPIRED_ACCESS_TOKEN, _)); GetNetworkAssignmentAndWaitForResponse(net::HTTP_UNAUTHORIZED, net::Error::OK, - "", "UserAuthT0kenz", kEngineVersion); + "", kTestAuthToken, kEngineVersion); } TEST_F(AssignmentSourceTest, TestForbidden) { EXPECT_CALL(*this, AssignmentResponse( AssignmentSource::Result::RESULT_USER_INVALID, _)); GetNetworkAssignmentAndWaitForResponse(net::HTTP_FORBIDDEN, net::Error::OK, - "", "UserAuthT0kenz", kEngineVersion); + "", kTestAuthToken, kEngineVersion); } TEST_F(AssignmentSourceTest, TestTooManyRequests) { EXPECT_CALL(*this, AssignmentResponse( AssignmentSource::Result::RESULT_OUT_OF_VMS, _)); GetNetworkAssignmentAndWaitForResponse(static_cast<net::HttpStatusCode>(429), - net::Error::OK, "", "UserAuthT0kenz", + net::Error::OK, "", kTestAuthToken, kEngineVersion); } @@ -290,7 +312,7 @@ TEST_F(AssignmentSourceTest, TestInternalServerError) { EXPECT_CALL(*this, AssignmentResponse( AssignmentSource::Result::RESULT_SERVER_ERROR, _)); GetNetworkAssignmentAndWaitForResponse(net::HTTP_INTERNAL_SERVER_ERROR, - net::Error::OK, "", "UserAuthT0kenz", + net::Error::OK, "", kTestAuthToken, kEngineVersion); } @@ -298,56 +320,62 @@ TEST_F(AssignmentSourceTest, TestUnexpectedNetCodeFallback) { EXPECT_CALL(*this, AssignmentResponse( AssignmentSource::Result::RESULT_BAD_RESPONSE, _)); GetNetworkAssignmentAndWaitForResponse(net::HTTP_NOT_IMPLEMENTED, - net::Error::OK, "", "UserAuthT0kenz", + net::Error::OK, "", kTestAuthToken, kEngineVersion); } TEST_F(AssignmentSourceTest, TestInvalidJsonResponse) { - Assignment assignment = BuildValidAssignment(); + Assignment assignment = BuildSslAssignment(); // Remove half the response. - std::string response = BuildResponseFromAssignment(assignment); + std::string response = ValueToString(*BuildAssignerResponse()); response = response.substr(response.size() / 2); EXPECT_CALL(*this, AssignmentResponse( AssignmentSource::Result::RESULT_BAD_RESPONSE, _)); GetNetworkAssignmentAndWaitForResponse(net::HTTP_OK, net::Error::OK, response, - "UserAuthT0kenz", kEngineVersion); + kTestAuthToken, kEngineVersion); } TEST_F(AssignmentSourceTest, TestMissingResponsePort) { - // Purposely do not add the 'port' field to the response. - base::DictionaryValue dict; - dict.SetString("clientToken", "SecretT0kenz"); - dict.SetString("host", "happywhales"); - dict.SetString("certificateFingerprint", "WhaleWhaleWhale"); - dict.SetString("certificate", "whaaaaaaaaaaaaale"); - - std::string response; - base::JSONWriter::Write(dict, &response); - + scoped_ptr<base::DictionaryValue> response = BuildAssignerResponse(); + response->Remove("port", nullptr); EXPECT_CALL(*this, AssignmentResponse( AssignmentSource::Result::RESULT_BAD_RESPONSE, _)); - GetNetworkAssignmentAndWaitForResponse(net::HTTP_OK, net::Error::OK, response, - "UserAuthT0kenz", kEngineVersion); + GetNetworkAssignmentAndWaitForResponse(net::HTTP_OK, net::Error::OK, + ValueToString(*response), + kTestAuthToken, kEngineVersion); } TEST_F(AssignmentSourceTest, TestInvalidIPAddress) { - // Purposely add an invalid IP field to the response. - base::DictionaryValue dict; - dict.SetString("clientToken", "SecretT0kenz"); - dict.SetString("host", "happywhales"); - dict.SetInteger("port", 500); - dict.SetString("certificateFingerprint", "WhaleWhaleWhale"); - dict.SetString("certificate", "whaaaaaaaaaaaaale"); + scoped_ptr<base::DictionaryValue> response = BuildAssignerResponse(); + response->SetString("host", "happywhales.test"); - std::string response; - base::JSONWriter::Write(dict, &response); + EXPECT_CALL(*this, AssignmentResponse( + AssignmentSource::Result::RESULT_BAD_RESPONSE, _)); + GetNetworkAssignmentAndWaitForResponse(net::HTTP_OK, net::Error::OK, + ValueToString(*response), + kTestAuthToken, kEngineVersion); +} +TEST_F(AssignmentSourceTest, TestMissingCert) { + scoped_ptr<base::DictionaryValue> response = BuildAssignerResponse(); + response->Remove("certificate", nullptr); EXPECT_CALL(*this, AssignmentResponse( AssignmentSource::Result::RESULT_BAD_RESPONSE, _)); - GetNetworkAssignmentAndWaitForResponse(net::HTTP_OK, net::Error::OK, response, - "UserAuthT0kenz", kEngineVersion); + GetNetworkAssignmentAndWaitForResponse(net::HTTP_OK, net::Error::OK, + ValueToString(*response), + kTestAuthToken, kEngineVersion); +} + +TEST_F(AssignmentSourceTest, TestInvalidCert) { + scoped_ptr<base::DictionaryValue> response = BuildAssignerResponse(); + response->SetString("certificate", "h4x0rz!"); + EXPECT_CALL(*this, AssignmentResponse( + AssignmentSource::Result::RESULT_INVALID_CERT, _)); + GetNetworkAssignmentAndWaitForResponse(net::HTTP_OK, net::Error::OK, + ValueToString(*response), + kTestAuthToken, kEngineVersion); } } // namespace diff --git a/blimp/client/session/blimp_client_session.cc b/blimp/client/session/blimp_client_session.cc index 29c0b32..7846d59 100644 --- a/blimp/client/session/blimp_client_session.cc +++ b/blimp/client/session/blimp_client_session.cc @@ -22,6 +22,7 @@ #include "blimp/net/client_connection_manager.h" #include "blimp/net/common.h" #include "blimp/net/null_blimp_message_processor.h" +#include "blimp/net/ssl_client_transport.h" #include "blimp/net/tcp_client_transport.h" #include "net/base/address_list.h" #include "net/base/ip_address.h" @@ -66,7 +67,6 @@ class ClientNetworkComponents { // they are used from the UI thread. std::vector<scoped_ptr<BlimpMessageThreadPipe>> outgoing_pipes_; std::vector<scoped_ptr<BlimpMessageProcessor>> outgoing_message_processors_; - DISALLOW_COPY_AND_ASSIGN(ClientNetworkComponents); }; @@ -81,8 +81,20 @@ void ClientNetworkComponents::ConnectWithAssignment( DCHECK(connection_manager_); connection_manager_->set_client_token(assignment.client_token); - connection_manager_->AddTransport(make_scoped_ptr(new TCPClientTransport( - net::AddressList(assignment.ip_endpoint), nullptr))); + switch (assignment.transport_protocol) { + case Assignment::SSL: + DCHECK(assignment.cert); + connection_manager_->AddTransport(make_scoped_ptr(new SSLClientTransport( + assignment.engine_endpoint, std::move(assignment.cert), nullptr))); + break; + case Assignment::TCP: + connection_manager_->AddTransport(make_scoped_ptr( + new TCPClientTransport(assignment.engine_endpoint, nullptr))); + break; + case Assignment::UNKNOWN: + DLOG(FATAL) << "Uknown transport type."; + break; + } connection_manager_->Connect(); } @@ -118,8 +130,8 @@ BlimpClientSession::BlimpClientSession() options.message_loop_type = base::MessageLoop::TYPE_IO; io_thread_.StartWithOptions(options); - assignment_source_.reset(new AssignmentSource( - base::ThreadTaskRunnerHandle::Get(), io_thread_.task_runner())); + assignment_source_.reset( + new AssignmentSource(io_thread_.task_runner(), io_thread_.task_runner())); // Register features' message senders and receivers. tab_control_feature_->set_outgoing_message_processor( diff --git a/blimp/client/session/test_selfsigned_cert.pem b/blimp/client/session/test_selfsigned_cert.pem new file mode 100644 index 0000000..de0e79f --- /dev/null +++ b/blimp/client/session/test_selfsigned_cert.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBmjCCAQOgAwIBAgIBATANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDEwh1bml0 +dGVzdDAeFw0xMDEyMjMwMjI5NDhaFw0xMDEyMjQwMjI5NDhaMBMxETAPBgNVBAMT +CHVuaXR0ZXN0MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDUrDNORwXwkey6 +1BtbawRswIkKE81+eZhlIOpMyKayUhi4qB5qaAg0Mt0Of7SwXYA/Nk0ADzcbI+jn +bl8y1gSaHN33I/OO9bEEwBtL2c+iF0Z9tI4iQ1lU2LQ2qiW8nfdQ21HUFbcnkk31 +vE8wNzh3c332RgVzvo5nzypVkamLgQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBABu5 +toCzcK6zKviZe+Uj5EVRUMwDywwCA7FadW7JmgCVt6yQ9YXgkqZelk0aodKZs+eS +WHuyx0EKVuZzaIiI2b/PKnfGVIvAu5Svzdqc8mlwEy4cBYoVFnFOIMzN93p9uUER +Y8o1fBKQE2LhRv3v86PYez5EI7xbUc/5ai+9LkXe +-----END CERTIFICATE----- |