Honor SSL config settings for ValidatorSites:

FIXES: #2990

* refactor common SSL client setup
* enable SSL in unit-test http server
* add tests for SSLHTTPDownloader
* misc test refactoring
This commit is contained in:
Mike Ellery
2019-08-05 09:47:09 -07:00
parent fc7ecd672a
commit 9213c49ca1
20 changed files with 1135 additions and 534 deletions

View File

@@ -2041,6 +2041,7 @@ if (unity)
src/test/unity/crypto_test_unity.cpp src/test/unity/crypto_test_unity.cpp
src/test/unity/json_test_unity.cpp src/test/unity/json_test_unity.cpp
src/test/unity/ledger_test_unity.cpp src/test/unity/ledger_test_unity.cpp
src/test/unity/net_test_unity.cpp
src/test/unity/nodestore_test_unity.cpp src/test/unity/nodestore_test_unity.cpp
src/test/unity/overlay_test_unity.cpp src/test/unity/overlay_test_unity.cpp
src/test/unity/peerfinder_test_unity.cpp src/test/unity/peerfinder_test_unity.cpp
@@ -2547,6 +2548,11 @@ else ()
src/test/ledger/SHAMapV2_test.cpp src/test/ledger/SHAMapV2_test.cpp
src/test/ledger/SkipList_test.cpp src/test/ledger/SkipList_test.cpp
src/test/ledger/View_test.cpp src/test/ledger/View_test.cpp
#[===============================[
nounity, test sources:
subdir: net
#]===============================]
src/test/net/SSLHTTPDownloader_test.cpp
#[===============================[ #[===============================[
nounity, test sources: nounity, test sources:
subdir: nodestore subdir: nodestore

View File

@@ -508,8 +508,7 @@ public:
*validatorManifests_, *publisherManifests_, *timeKeeper_, *validatorManifests_, *publisherManifests_, *timeKeeper_,
logs_->journal("ValidatorList"), config_->VALIDATION_QUORUM)) logs_->journal("ValidatorList"), config_->VALIDATION_QUORUM))
, validatorSites_ (std::make_unique<ValidatorSite> ( , validatorSites_ (std::make_unique<ValidatorSite> (*this))
get_io_service (), *validators_, logs_->journal("ValidatorSite")))
, serverHandler_ (make_ServerHandler (*this, *m_networkOPs, get_io_service (), , serverHandler_ (make_ServerHandler (*this, *m_networkOPs, get_io_service (),
*m_jobQueue, *m_networkOPs, *m_resourceManager, *m_jobQueue, *m_networkOPs, *m_resourceManager,

View File

@@ -22,10 +22,13 @@
#include <ripple/app/misc/ValidatorList.h> #include <ripple/app/misc/ValidatorList.h>
#include <ripple/app/misc/detail/Work.h> #include <ripple/app/misc/detail/Work.h>
#include <ripple/app/main/Application.h>
#include <ripple/basics/Log.h> #include <ripple/basics/Log.h>
#include <ripple/basics/StringUtilities.h> #include <ripple/basics/StringUtilities.h>
#include <ripple/core/Config.h>
#include <ripple/json/json_value.h> #include <ripple/json/json_value.h>
#include <boost/asio.hpp> #include <boost/asio.hpp>
#include <boost/optional.hpp>
#include <mutex> #include <mutex>
#include <memory> #include <memory>
@@ -106,9 +109,9 @@ private:
boost::optional<Status> lastRefreshStatus; boost::optional<Status> lastRefreshStatus;
}; };
boost::asio::io_service& ios_; Application& app_;
ValidatorList& validators_;
beast::Journal j_; beast::Journal j_;
std::mutex mutable sites_mutex_; std::mutex mutable sites_mutex_;
std::mutex mutable state_mutex_; std::mutex mutable state_mutex_;
@@ -131,9 +134,8 @@ private:
public: public:
ValidatorSite ( ValidatorSite (
boost::asio::io_service& ios, Application& app,
ValidatorList& validators, boost::optional<beast::Journal> j = boost::none,
beast::Journal j,
std::chrono::seconds timeout = std::chrono::seconds{20}); std::chrono::seconds timeout = std::chrono::seconds{20});
~ValidatorSite (); ~ValidatorSite ();

View File

@@ -21,8 +21,9 @@
#define RIPPLE_APP_MISC_DETAIL_WORKSSL_H_INCLUDED #define RIPPLE_APP_MISC_DETAIL_WORKSSL_H_INCLUDED
#include <ripple/app/misc/detail/WorkBase.h> #include <ripple/app/misc/detail/WorkBase.h>
#include <ripple/net/RegisterSSLCerts.h>
#include <ripple/basics/contract.h> #include <ripple/basics/contract.h>
#include <ripple/core/Config.h>
#include <ripple/net/HTTPClientSSLContext.h>
#include <boost/asio/ssl.hpp> #include <boost/asio/ssl.hpp>
#include <boost/bind.hpp> #include <boost/bind.hpp>
#include <boost/format.hpp> #include <boost/format.hpp>
@@ -31,24 +32,6 @@ namespace ripple {
namespace detail { namespace detail {
class SSLContext : public boost::asio::ssl::context
{
public:
SSLContext(beast::Journal j)
: boost::asio::ssl::context(boost::asio::ssl::context::sslv23)
{
boost::system::error_code ec;
registerSSLCerts(*this, ec, j);
if (ec)
{
Throw<std::runtime_error> (
boost::str (boost::format (
"Failed to set_default_verify_paths: %s") %
ec.message ()));
}
}
};
// Work over SSL // Work over SSL
class WorkSSL : public WorkBase<WorkSSL> class WorkSSL : public WorkBase<WorkSSL>
, public std::enable_shared_from_this<WorkSSL> , public std::enable_shared_from_this<WorkSSL>
@@ -58,7 +41,7 @@ class WorkSSL : public WorkBase<WorkSSL>
private: private:
using stream_type = boost::asio::ssl::stream<socket_type&>; using stream_type = boost::asio::ssl::stream<socket_type&>;
SSLContext context_; HTTPClientSSLContext context_;
stream_type stream_; stream_type stream_;
public: public:
@@ -68,6 +51,7 @@ public:
std::string const& port, std::string const& port,
boost::asio::io_service& ios, boost::asio::io_service& ios,
beast::Journal j, beast::Journal j,
Config const& config,
callback_type cb); callback_type cb);
~WorkSSL() = default; ~WorkSSL() = default;
@@ -83,15 +67,6 @@ private:
void void
onHandshake(error_code const& ec); onHandshake(error_code const& ec);
static bool
rfc2818_verify(
std::string const& domain,
bool preverified,
boost::asio::ssl::verify_context& ctx)
{
return boost::asio::ssl::rfc2818_verification(domain)(preverified, ctx);
}
}; };
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -102,24 +77,24 @@ WorkSSL::WorkSSL(
std::string const& port, std::string const& port,
boost::asio::io_service& ios, boost::asio::io_service& ios,
beast::Journal j, beast::Journal j,
Config const& config,
callback_type cb) callback_type cb)
: WorkBase(host, path, port, ios, cb) : WorkBase(host, path, port, ios, cb)
, context_(j) , context_(config, j, boost::asio::ssl::context::tlsv12_client)
, stream_(socket_, context_) , stream_(socket_, context_.context())
{ {
// Set SNI hostname auto ec = context_.preConnectVerify(stream_, host_);
SSL_set_tlsext_host_name(stream_.native_handle(), host.c_str()); if (ec)
stream_.set_verify_mode (boost::asio::ssl::verify_peer); Throw<std::runtime_error> (
stream_.set_verify_callback( std::bind ( boost::str (boost::format ("preConnectVerify: %s") % ec.message ()));
&WorkSSL::rfc2818_verify, host_,
std::placeholders::_1, std::placeholders::_2));
} }
void void
WorkSSL::onConnect(error_code const& ec) WorkSSL::onConnect(error_code const& ec)
{ {
if (ec) auto err = ec ? ec : context_.postConnectVerify(stream_, host_);
return fail(ec); if (err)
return fail(err);
stream_.async_handshake( stream_.async_handshake(
boost::asio::ssl::stream_base::client, boost::asio::ssl::stream_base::client,

View File

@@ -88,18 +88,16 @@ ValidatorSite::Site::Site (std::string uri)
} }
ValidatorSite::ValidatorSite ( ValidatorSite::ValidatorSite (
boost::asio::io_service& ios, Application& app,
ValidatorList& validators, boost::optional<beast::Journal> j,
beast::Journal j,
std::chrono::seconds timeout) std::chrono::seconds timeout)
: ios_ (ios) : app_ {app}
, validators_ (validators) , j_ {j ? *j : app_.logs().journal("ValidatorSite") }
, j_ (j) , timer_ {app_.getIOService()}
, timer_ (ios_) , fetching_ {false}
, fetching_ (false) , pending_ {false}
, pending_ (false) , stopping_ {false}
, stopping_ (false) , requestTimeout_ {timeout}
, requestTimeout_ (timeout)
{ {
} }
@@ -258,12 +256,14 @@ ValidatorSite::makeRequest (
if (resource->pUrl.scheme == "https") if (resource->pUrl.scheme == "https")
{ {
// can throw...
sp = std::make_shared<detail::WorkSSL>( sp = std::make_shared<detail::WorkSSL>(
resource->pUrl.domain, resource->pUrl.domain,
resource->pUrl.path, resource->pUrl.path,
std::to_string(*resource->pUrl.port), std::to_string(*resource->pUrl.port),
ios_, app_.getIOService(),
j_, j_,
app_.config(),
onFetch); onFetch);
} }
else if(resource->pUrl.scheme == "http") else if(resource->pUrl.scheme == "http")
@@ -272,7 +272,7 @@ ValidatorSite::makeRequest (
resource->pUrl.domain, resource->pUrl.domain,
resource->pUrl.path, resource->pUrl.path,
std::to_string(*resource->pUrl.port), std::to_string(*resource->pUrl.port),
ios_, app_.getIOService(),
onFetch); onFetch);
} }
else else
@@ -280,7 +280,7 @@ ValidatorSite::makeRequest (
BOOST_ASSERT(resource->pUrl.scheme == "file"); BOOST_ASSERT(resource->pUrl.scheme == "file");
sp = std::make_shared<detail::WorkFile>( sp = std::make_shared<detail::WorkFile>(
resource->pUrl.path, resource->pUrl.path,
ios_, app_.getIOService(),
onFetchFile); onFetchFile);
} }
@@ -336,7 +336,7 @@ ValidatorSite::onTimer (
sites_[siteIdx].nextRefresh = sites_[siteIdx].nextRefresh =
clock_type::now() + sites_[siteIdx].refreshInterval; clock_type::now() + sites_[siteIdx].refreshInterval;
sites_[siteIdx].redirCount = 0; sites_[siteIdx].redirCount = 0;
// the WorkSSL client can throw if SSL init fails // the WorkSSL client ctor can throw if SSL init fails
makeRequest(sites_[siteIdx].startingResource, siteIdx, lock); makeRequest(sites_[siteIdx].startingResource, siteIdx, lock);
} }
catch (std::exception &) catch (std::exception &)
@@ -376,7 +376,7 @@ ValidatorSite::parseJsonResponse (
throw std::runtime_error{"missing fields"}; throw std::runtime_error{"missing fields"};
} }
auto const disp = validators_.applyList ( auto const disp = app_.validators().applyList (
body["manifest"].asString (), body["manifest"].asString (),
body["blob"].asString (), body["blob"].asString (),
body["signature"].asString(), body["signature"].asString(),

View File

@@ -116,40 +116,6 @@ public:
return lowest_layer ().cancel (ec); return lowest_layer ().cancel (ec);
} }
static bool rfc2818_verify (std::string const& domain, bool preverified,
boost::asio::ssl::verify_context& ctx, beast::Journal j)
{
using namespace ripple;
if (boost::asio::ssl::rfc2818_verification (domain) (preverified, ctx))
return true;
JLOG (j.warn()) <<
"Outbound SSL connection to " << domain <<
" fails certificate verification";
return false;
}
boost::system::error_code verify (std::string const& strDomain)
{
boost::system::error_code ec;
mSocket->set_verify_mode (boost::asio::ssl::verify_peer);
// XXX Verify semantics of RFC 2818 are what we want.
mSocket->set_verify_callback (
std::bind (&rfc2818_verify, strDomain,
std::placeholders::_1, std::placeholders::_2, j_), ec);
return ec;
}
void setTLSHostName(std::string const & host)
{
SSL_set_tlsext_host_name(mSocket->native_handle(), host.c_str());
}
void async_handshake (handshake_type type, callback cbFunc) void async_handshake (handshake_type type, callback cbFunc)
{ {
if ((type == ssl_socket::client) || (mSecure)) if ((type == ssl_socket::client) || (mSecure))

View File

@@ -0,0 +1,191 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2019 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#ifndef RIPPLE_NET_HTTPCLIENTSSLCONTEXT_H_INCLUDED
#define RIPPLE_NET_HTTPCLIENTSSLCONTEXT_H_INCLUDED
#include <ripple/basics/contract.h>
#include <ripple/basics/Log.h>
#include <ripple/core/Config.h>
#include <ripple/net/RegisterSSLCerts.h>
#include <boost/asio.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/format.hpp>
namespace ripple {
class HTTPClientSSLContext
{
public:
explicit
HTTPClientSSLContext (
Config const& config,
beast::Journal j,
boost::asio::ssl::context_base::method method =
boost::asio::ssl::context::sslv23)
: ssl_context_ {method}
, j_(j)
, verify_ {config.SSL_VERIFY}
{
boost::system::error_code ec;
if (config.SSL_VERIFY_FILE.empty ())
{
registerSSLCerts(ssl_context_, ec, j_);
if (ec && config.SSL_VERIFY_DIR.empty ())
Throw<std::runtime_error> (
boost::str (boost::format (
"Failed to set_default_verify_paths: %s") %
ec.message ()));
}
else
{
ssl_context_.load_verify_file (config.SSL_VERIFY_FILE);
}
if (! config.SSL_VERIFY_DIR.empty ())
{
ssl_context_.add_verify_path (config.SSL_VERIFY_DIR, ec);
if (ec)
Throw<std::runtime_error> (
boost::str (boost::format (
"Failed to add verify path: %s") % ec.message ()));
}
}
boost::asio::ssl::context& context()
{
return ssl_context_;
}
bool sslVerify() const
{
return verify_;
}
/**
* @brief invoked before connect/async_connect on an ssl stream
* to setup name verification.
*
* If we intend to verify the SSL connection, we need to set the
* default domain for server name indication *prior* to connecting
*
* @param strm asio ssl stream
* @param host hostname to verify
*
* @return error_code indicating failures, if any
*/
template<class T,
class = std::enable_if_t<
std::is_same<T, boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>::value ||
std::is_same<T, boost::asio::ssl::stream<boost::asio::ip::tcp::socket&>>::value
>
>
boost::system::error_code
preConnectVerify (
T& strm,
std::string const& host)
{
boost::system::error_code ec;
if (!SSL_set_tlsext_host_name(strm.native_handle(), host.c_str()))
{
ec.assign(static_cast<int>(
::ERR_get_error()), boost::asio::error::get_ssl_category());
}
else if (!sslVerify())
{
strm.set_verify_mode(boost::asio::ssl::verify_none, ec);
}
return ec;
}
template<class T,
class = std::enable_if_t<
std::is_same<T, boost::asio::ssl::stream<boost::asio::ip::tcp::socket>>::value ||
std::is_same<T, boost::asio::ssl::stream<boost::asio::ip::tcp::socket&>>::value
>
>
/**
* @brief invoked after connect/async_connect but before sending data
* on an ssl stream - to setup name verification.
*
* @param strm asio ssl stream
* @param host hostname to verify
*/
boost::system::error_code
postConnectVerify (
T& strm,
std::string const& host)
{
boost::system::error_code ec;
if (sslVerify())
{
strm.set_verify_mode (boost::asio::ssl::verify_peer, ec);
if (!ec)
{
strm.set_verify_callback (
std::bind (&rfc2818_verify, host,
std::placeholders::_1, std::placeholders::_2, j_), ec);
}
}
return ec;
}
/**
* @brief callback invoked for name verification - just passes through
* to the asio rfc2818 implementation.
*
* @param domain hostname expected
* @param preverified passed by implementation
* @param ctx passed by implementation
* @param j journal for logging
*/
static
bool
rfc2818_verify (
std::string const& domain,
bool preverified,
boost::asio::ssl::verify_context& ctx,
beast::Journal j)
{
if (boost::asio::ssl::rfc2818_verification (domain) (preverified, ctx))
return true;
JLOG (j.warn()) <<
"Outbound SSL connection to " << domain <<
" fails certificate verification";
return false;
}
private:
boost::asio::ssl::context ssl_context_;
beast::Journal j_;
const bool verify_;
};
} // ripple
#endif

View File

@@ -22,6 +22,7 @@
#include <ripple/basics/Log.h> #include <ripple/basics/Log.h>
#include <ripple/core/Config.h> #include <ripple/core/Config.h>
#include <ripple/net/HTTPClientSSLContext.h>
#include <boost/asio/connect.hpp> #include <boost/asio/connect.hpp>
#include <boost/asio/io_service.hpp> #include <boost/asio/io_service.hpp>
@@ -48,10 +49,8 @@ public:
SSLHTTPDownloader( SSLHTTPDownloader(
boost::asio::io_service& io_service, boost::asio::io_service& io_service,
beast::Journal j); beast::Journal j,
Config const& config);
bool
init(Config const& config);
bool bool
download( download(
@@ -63,12 +62,11 @@ public:
std::function<void(boost::filesystem::path)> complete); std::function<void(boost::filesystem::path)> complete);
private: private:
boost::asio::ssl::context ctx_; HTTPClientSSLContext ssl_ctx_;
boost::asio::io_service::strand strand_; boost::asio::io_service::strand strand_;
boost::optional< boost::optional<
boost::asio::ssl::stream<boost::asio::ip::tcp::socket>> stream_; boost::asio::ssl::stream<boost::asio::ip::tcp::socket>> stream_;
boost::beast::flat_buffer read_buf_; boost::beast::flat_buffer read_buf_;
bool ssl_verify_;
beast::Journal j_; beast::Journal j_;
void void

View File

@@ -21,8 +21,8 @@
#include <ripple/basics/Log.h> #include <ripple/basics/Log.h>
#include <ripple/basics/StringUtilities.h> #include <ripple/basics/StringUtilities.h>
#include <ripple/net/HTTPClient.h> #include <ripple/net/HTTPClient.h>
#include <ripple/net/HTTPClientSSLContext.h>
#include <ripple/net/AutoSocket.h> #include <ripple/net/AutoSocket.h>
#include <ripple/net/RegisterSSLCerts.h>
#include <ripple/beast/core/LexicalCast.h> #include <ripple/beast/core/LexicalCast.h>
#include <boost/asio.hpp> #include <boost/asio.hpp>
#include <boost/asio/ssl.hpp> #include <boost/asio/ssl.hpp>
@@ -32,61 +32,6 @@
namespace ripple { namespace ripple {
//
// Fetch a web page via http or https.
//
class HTTPClientSSLContext
{
public:
explicit
HTTPClientSSLContext (Config const& config, beast::Journal j)
: m_context (boost::asio::ssl::context::sslv23)
, verify_ (config.SSL_VERIFY)
{
boost::system::error_code ec;
if (config.SSL_VERIFY_FILE.empty ())
{
registerSSLCerts(m_context, ec, j);
if (ec && config.SSL_VERIFY_DIR.empty ())
Throw<std::runtime_error> (
boost::str (boost::format (
"Failed to set_default_verify_paths: %s") %
ec.message ()));
}
else
{
m_context.load_verify_file (config.SSL_VERIFY_FILE);
}
if (! config.SSL_VERIFY_DIR.empty ())
{
m_context.add_verify_path (config.SSL_VERIFY_DIR, ec);
if (ec)
Throw<std::runtime_error> (
boost::str (boost::format (
"Failed to add verify path: %s") % ec.message ()));
}
}
boost::asio::ssl::context& context()
{
return m_context;
}
bool sslVerify() const
{
return verify_;
}
private:
boost::asio::ssl::context m_context;
bool verify_;
};
boost::optional<HTTPClientSSLContext> httpClientSSLContext; boost::optional<HTTPClientSSLContext> httpClientSSLContext;
void HTTPClient::initializeSSLContext (Config const& config, beast::Journal j) void HTTPClient::initializeSSLContext (Config const& config, beast::Journal j)
@@ -94,6 +39,10 @@ void HTTPClient::initializeSSLContext (Config const& config, beast::Journal j)
httpClientSSLContext.emplace (config, j); httpClientSSLContext.emplace (config, j);
} }
//------------------------------------------------------------------------------
//
// Fetch a web page via http or https.
//
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
class HTTPClientImp class HTTPClientImp
@@ -113,8 +62,6 @@ public:
, mDeadline (io_service) , mDeadline (io_service)
, j_ (j) , j_ (j)
{ {
if (!httpClientSSLContext->sslVerify())
mSocket.SSLSocket ().set_verify_mode (boost::asio::ssl::verify_none);
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -127,19 +74,24 @@ public:
osRequest << osRequest <<
"GET " << strPath << " HTTP/1.0\r\n" "GET " << strPath << " HTTP/1.0\r\n"
"Host: " << strHost << "\r\n" "Host: " << strHost << "\r\n"
"Accept: */*\r\n" // YYY Do we need this line? "Accept: */*\r\n" // YYY Do we need this line?
"Connection: close\r\n\r\n"; "Connection: close\r\n\r\n";
} }
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
void request ( void
request(
bool bSSL, bool bSSL,
std::deque<std::string> deqSites, std::deque<std::string> deqSites,
std::function<void (boost::asio::streambuf& sb, std::string const& strHost)> build, std::function<void (
boost::asio::streambuf& sb,
std::string const& strHost)> build,
std::chrono::seconds timeout, std::chrono::seconds timeout,
std::function<bool (const boost::system::error_code& ecResult, std::function<bool (
int iStatus, std::string const& strData)> complete) const boost::system::error_code& ecResult,
int iStatus,
std::string const& strData)> complete)
{ {
mSSL = bSSL; mSSL = bSSL;
mDeqSites = deqSites; mDeqSites = deqSites;
@@ -152,23 +104,28 @@ public:
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
void get ( void
bool bSSL, get(bool bSSL,
std::deque<std::string> deqSites, std::deque<std::string> deqSites,
std::string const& strPath, std::string const& strPath,
std::chrono::seconds timeout, std::chrono::seconds timeout,
std::function<bool (const boost::system::error_code& ecResult, int iStatus, std::function<bool (
const boost::system::error_code& ecResult,
int iStatus,
std::string const& strData)> complete) std::string const& strData)> complete)
{ {
mComplete = complete; mComplete = complete;
mTimeout = timeout; mTimeout = timeout;
request ( request (
bSSL, bSSL,
deqSites, deqSites,
std::bind (&HTTPClientImp::makeGet, shared_from_this (), strPath, std::bind(
std::placeholders::_1, std::placeholders::_2), &HTTPClientImp::makeGet,
shared_from_this(),
strPath,
std::placeholders::_1,
std::placeholders::_2),
timeout, timeout,
complete); complete);
} }
@@ -225,7 +182,8 @@ public:
} }
else if (ecResult) else if (ecResult)
{ {
JLOG (j_.trace()) << "Deadline error: " << mDeqSites[0] << ": " << ecResult.message (); JLOG (j_.trace()) << "Deadline error: "
<< mDeqSites[0] << ": " << ecResult.message ();
// Can't do anything sound. // Can't do anything sound.
abort (); abort ();
@@ -236,7 +194,9 @@ public:
// Mark us as shutting down. // Mark us as shutting down.
// XXX Use our own error code. // XXX Use our own error code.
mShutdown = boost::system::error_code (boost::system::errc::bad_address, boost::system::system_category ()); mShutdown = boost::system::error_code {
boost::system::errc::bad_address,
boost::system::system_category ()};
// Cancel any resolving. // Cancel any resolving.
mResolver.cancel (); mResolver.cancel ();
@@ -256,7 +216,8 @@ public:
{ {
if (ecResult) if (ecResult)
{ {
JLOG (j_.trace()) << "Shutdown error: " << mDeqSites[0] << ": " << ecResult.message (); JLOG (j_.trace()) << "Shutdown error: "
<< mDeqSites[0] << ": " << ecResult.message ();
} }
} }
@@ -266,11 +227,18 @@ public:
) )
{ {
if (!mShutdown) if (!mShutdown)
mShutdown = ecResult; {
mShutdown =
ecResult ?
ecResult :
httpClientSSLContext->preConnectVerify (
mSocket.SSLSocket(), mDeqSites[0]);
}
if (mShutdown) if (mShutdown)
{ {
JLOG (j_.trace()) << "Resolve error: " << mDeqSites[0] << ": " << mShutdown.message (); JLOG (j_.trace()) << "Resolve error: "
<< mDeqSites[0] << ": " << mShutdown.message ();
invokeComplete (mShutdown); invokeComplete (mShutdown);
} }
@@ -278,12 +246,6 @@ public:
{ {
JLOG (j_.trace()) << "Resolve complete."; JLOG (j_.trace()) << "Resolve complete.";
// If we intend to verify the SSL connection, we need to
// set the default domain for server name indication *prior* to
// connecting
if (httpClientSSLContext->sslVerify())
mSocket.setTLSHostName(mDeqSites[0]);
boost::asio::async_connect ( boost::asio::async_connect (
mSocket.lowest_layer (), mSocket.lowest_layer (),
itrEndpoint, itrEndpoint,
@@ -308,14 +270,13 @@ public:
{ {
JLOG (j_.trace()) << "Connected."; JLOG (j_.trace()) << "Connected.";
if (httpClientSSLContext->sslVerify ()) mShutdown = httpClientSSLContext->postConnectVerify (
{ mSocket.SSLSocket(), mDeqSites[0]);
mShutdown = mSocket.verify (mDeqSites[0]);
if (mShutdown) if (mShutdown)
{ {
JLOG (j_.trace()) << "set_verify_callback: " << mDeqSites[0] << ": " << mShutdown.message (); JLOG (j_.trace()) << "postConnectVerify: "
} << mDeqSites[0] << ": " << mShutdown.message ();
} }
} }
@@ -364,7 +325,9 @@ public:
} }
} }
void handleWrite (const boost::system::error_code& ecResult, std::size_t bytes_transferred) void handleWrite (
const boost::system::error_code& ecResult,
std::size_t bytes_transferred)
{ {
if (!mShutdown) if (!mShutdown)
mShutdown = ecResult; mShutdown = ecResult;
@@ -389,24 +352,32 @@ public:
} }
} }
void handleHeader (const boost::system::error_code& ecResult, std::size_t bytes_transferred) void handleHeader (
const boost::system::error_code& ecResult,
std::size_t bytes_transferred)
{ {
std::string strHeader ((std::istreambuf_iterator<char> (&mHeader)), std::istreambuf_iterator<char> ()); std::string strHeader {
{std::istreambuf_iterator<char> (&mHeader)},
std::istreambuf_iterator<char> ()};
JLOG (j_.trace()) << "Header: \"" << strHeader << "\""; JLOG (j_.trace()) << "Header: \"" << strHeader << "\"";
static boost::regex reStatus ("\\`HTTP/1\\S+ (\\d{3}) .*\\'"); // HTTP/1.1 200 OK static boost::regex reStatus {
static boost::regex reSize ("\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'"); "\\`HTTP/1\\S+ (\\d{3}) .*\\'"}; // HTTP/1.1 200 OK
static boost::regex reBody ("\\`.*\\r\\n\\r\\n(.*)\\'"); static boost::regex reSize {
"\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'"};
static boost::regex reBody {
"\\`.*\\r\\n\\r\\n(.*)\\'"};
boost::smatch smMatch; boost::smatch smMatch;
// Match status code.
bool bMatch = boost::regex_match (strHeader, smMatch, reStatus); // Match status code. if (!boost::regex_match (strHeader, smMatch, reStatus))
if (!bMatch)
{ {
// XXX Use our own error code. // XXX Use our own error code.
JLOG (j_.trace()) << "No status code"; JLOG (j_.trace()) << "No status code";
invokeComplete (boost::system::error_code (boost::system::errc::bad_address, boost::system::system_category ())); invokeComplete (
boost::system::error_code {
boost::system::errc::bad_address,
boost::system::system_category ()});
return; return;
} }
@@ -416,7 +387,8 @@ public:
mBody = smMatch[1]; mBody = smMatch[1];
if (boost::regex_match (strHeader, smMatch, reSize)) if (boost::regex_match (strHeader, smMatch, reSize))
mResponseSize = beast::lexicalCastThrow <int> (std::string(smMatch[1])); mResponseSize =
beast::lexicalCastThrow <int> (std::string(smMatch[1]));
if (mResponseSize == 0) if (mResponseSize == 0)
{ {
@@ -440,7 +412,9 @@ public:
} }
} }
void handleData (const boost::system::error_code& ecResult, std::size_t bytes_transferred) void handleData (
const boost::system::error_code& ecResult,
std::size_t bytes_transferred)
{ {
if (!mShutdown) if (!mShutdown)
mShutdown = ecResult; mShutdown = ecResult;
@@ -460,14 +434,19 @@ public:
else else
{ {
mResponse.commit (bytes_transferred); mResponse.commit (bytes_transferred);
std::string strBody ((std::istreambuf_iterator<char> (&mResponse)), std::istreambuf_iterator<char> ()); std::string strBody {
{std::istreambuf_iterator<char> (&mResponse)},
std::istreambuf_iterator<char> ()};
invokeComplete (ecResult, mStatus, mBody + strBody); invokeComplete (ecResult, mStatus, mBody + strBody);
} }
} }
} }
// Call cancel the deadline timer and invoke the completion routine. // Call cancel the deadline timer and invoke the completion routine.
void invokeComplete (const boost::system::error_code& ecResult, int iStatus = 0, std::string const& strData = "") void invokeComplete (
const boost::system::error_code& ecResult,
int iStatus = 0,
std::string const& strData = "")
{ {
boost::system::error_code ecCancel; boost::system::error_code ecCancel;
@@ -475,10 +454,12 @@ public:
if (ecCancel) if (ecCancel)
{ {
JLOG (j_.trace()) << "invokeComplete: Deadline cancel error: " << ecCancel.message (); JLOG (j_.trace()) << "invokeComplete: Deadline cancel error: "
<< ecCancel.message ();
} }
JLOG (j_.debug()) << "invokeComplete: Deadline popping: " << mDeqSites.size (); JLOG (j_.debug()) << "invokeComplete: Deadline popping: "
<< mDeqSites.size ();
if (!mDeqSites.empty ()) if (!mDeqSites.empty ())
{ {
@@ -492,7 +473,8 @@ public:
// ecResult: !0 = had an error, last entry // ecResult: !0 = had an error, last entry
// iStatus: result, if no error // iStatus: result, if no error
// strData: data, if no error // strData: data, if no error
bAgain = mComplete && mComplete (ecResult ? ecResult : ecCancel, iStatus, strData); bAgain = mComplete &&
mComplete (ecResult ? ecResult : ecCancel, iStatus, strData);
} }
if (!mDeqSites.empty () && bAgain) if (!mDeqSites.empty () && bAgain)
@@ -515,8 +497,12 @@ private:
const unsigned short mPort; const unsigned short mPort;
int mResponseSize; int mResponseSize;
int mStatus; int mStatus;
std::function<void (boost::asio::streambuf& sb, std::string const& strHost)> mBuild; std::function<void (boost::asio::streambuf& sb, std::string const& strHost)>
std::function<bool (const boost::system::error_code& ecResult, int iStatus, std::string const& strData)> mComplete; mBuild;
std::function<bool (
const boost::system::error_code& ecResult,
int iStatus,
std::string const& strData)> mComplete;
boost::asio::basic_waitable_timer<std::chrono::steady_clock> mDeadline; boost::asio::basic_waitable_timer<std::chrono::steady_clock> mDeadline;
@@ -566,15 +552,19 @@ void HTTPClient::get (
client->get (bSSL, deqSites, strPath, timeout, complete); client->get (bSSL, deqSites, strPath, timeout, complete);
} }
void HTTPClient::request ( void
HTTPClient::request(
bool bSSL, bool bSSL,
boost::asio::io_service& io_service, boost::asio::io_service& io_service,
std::string strSite, std::string strSite,
const unsigned short port, const unsigned short port,
std::function<void (boost::asio::streambuf& sb, std::string const& strHost)> setRequest, std::function<void (boost::asio::streambuf& sb, std::string const& strHost)>
setRequest,
std::size_t responseMax, std::size_t responseMax,
std::chrono::seconds timeout, std::chrono::seconds timeout,
std::function<bool (const boost::system::error_code& ecResult, int iStatus, std::function<bool (
const boost::system::error_code& ecResult,
int iStatus,
std::string const& strData)> complete, std::string const& strData)> complete,
beast::Journal& j) beast::Journal& j)
{ {

View File

@@ -18,54 +18,20 @@
//============================================================================== //==============================================================================
#include <ripple/net/SSLHTTPDownloader.h> #include <ripple/net/SSLHTTPDownloader.h>
#include <ripple/net/RegisterSSLCerts.h>
#include <boost/asio/ssl.hpp> #include <boost/asio/ssl.hpp>
namespace ripple { namespace ripple {
SSLHTTPDownloader::SSLHTTPDownloader( SSLHTTPDownloader::SSLHTTPDownloader(
boost::asio::io_service& io_service, boost::asio::io_service& io_service,
beast::Journal j) beast::Journal j,
: ctx_(boost::asio::ssl::context::tlsv12_client) Config const& config)
: ssl_ctx_(config, j, boost::asio::ssl::context::tlsv12_client)
, strand_(io_service) , strand_(io_service)
, j_(j) , j_(j)
{ {
} }
bool
SSLHTTPDownloader::init(Config const& config)
{
boost::system::error_code ec;
if (config.SSL_VERIFY_FILE.empty())
{
registerSSLCerts(ctx_, ec, j_);
if (ec && config.SSL_VERIFY_DIR.empty())
{
JLOG(j_.error()) <<
"Failed to set_default_verify_paths: " <<
ec.message();
return false;
}
}
else
ctx_.load_verify_file(config.SSL_VERIFY_FILE);
if (!config.SSL_VERIFY_DIR.empty())
{
ctx_.add_verify_path(config.SSL_VERIFY_DIR, ec);
if (ec)
{
JLOG(j_.error()) <<
"Failed to add verify path: " <<
ec.message();
return false;
}
}
ssl_verify_ = config.SSL_VERIFY;
return true;
}
bool bool
SSLHTTPDownloader::download( SSLHTTPDownloader::download(
std::string const& host, std::string const& host,
@@ -139,7 +105,7 @@ SSLHTTPDownloader::do_session(
try try
{ {
stream_.emplace(strand_.context(), ctx_); stream_.emplace(strand_.context(), ssl_ctx_.context());
} }
catch (std::exception const& e) catch (std::exception const& e)
{ {
@@ -147,40 +113,18 @@ SSLHTTPDownloader::do_session(
std::string("exception: ") + e.what()); std::string("exception: ") + e.what());
} }
if (ssl_verify_) ec = ssl_ctx_.preConnectVerify(*stream_, host);
{ if (ec)
// If we intend to verify the SSL connection, we need to set the return fail(dstPath, complete, ec, "preConnectVerify");
// default domain for server name indication *prior* to connecting
if (!SSL_set_tlsext_host_name(stream_->native_handle(), host.c_str()))
{
ec.assign(static_cast<int>(
::ERR_get_error()), boost::asio::error::get_ssl_category());
return fail(dstPath, complete, ec, "SSL_set_tlsext_host_name");
}
}
else
{
stream_->set_verify_mode(boost::asio::ssl::verify_none, ec);
if (ec)
return fail(dstPath, complete, ec, "set_verify_mode");
}
boost::asio::async_connect( boost::asio::async_connect(
stream_->next_layer(), results.begin(), results.end(), yield[ec]); stream_->next_layer(), results.begin(), results.end(), yield[ec]);
if (ec) if (ec)
return fail(dstPath, complete, ec, "async_connect"); return fail(dstPath, complete, ec, "async_connect");
if (ssl_verify_) ec = ssl_ctx_.postConnectVerify(*stream_, host);
{ if (ec)
stream_->set_verify_mode(boost::asio::ssl::verify_peer, ec); return fail(dstPath, complete, ec, "postConnectVerify");
if (ec)
return fail(dstPath, complete, ec, "set_verify_mode");
stream_->set_verify_callback(
boost::asio::ssl::rfc2818_verification(host.c_str()), ec);
if (ec)
return fail(dstPath, complete, ec, "set_verify_callback");
}
stream_->async_handshake(ssl::stream_base::client, yield[ec]); stream_->async_handshake(ssl::stream_base::client, yield[ec]);
if (ec) if (ec)

View File

@@ -113,6 +113,13 @@ ShardArchiveHandler::start()
// Create temp root download directory // Create temp root download directory
create_directory(downloadDir_); create_directory(downloadDir_);
if (!downloader_)
{
// will throw if can't initialize ssl context
downloader_ = std::make_shared<SSLHTTPDownloader>(
app_.getIOService(), j_, app_.config());
}
} }
catch (std::exception const& e) catch (std::exception const& e)
{ {
@@ -121,16 +128,6 @@ ShardArchiveHandler::start()
return false; return false;
} }
if (!downloader_)
{
downloader_ = std::make_shared<SSLHTTPDownloader>(
app_.getIOService(), j_);
if (!downloader_->init(app_.config()))
{
downloader_.reset();
return false;
}
}
return next(lock); return next(lock);
} }

View File

@@ -60,50 +60,6 @@ private:
using Validator = TrustedPublisherServer::Validator; using Validator = TrustedPublisherServer::Validator;
static
PublicKey
randomNode ()
{
return derivePublicKey (KeyType::secp256k1, randomSecretKey());
}
static
std::string
makeManifestString (
PublicKey const& pk,
SecretKey const& sk,
PublicKey const& spk,
SecretKey const& ssk,
int seq)
{
STObject st(sfGeneric);
st[sfSequence] = seq;
st[sfPublicKey] = pk;
st[sfSigningPubKey] = spk;
sign(st, HashPrefix::manifest, *publicKeyType(spk), ssk);
sign(st, HashPrefix::manifest, *publicKeyType(pk), sk,
sfMasterSignature);
Serializer s;
st.add(s);
return base64_encode (std::string(
static_cast<char const*> (s.data()), s.size()));
}
static
Validator
randomValidator ()
{
auto const secret = randomSecretKey();
auto const masterPublic =
derivePublicKey(KeyType::ed25519, secret);
auto const signingKeys = randomKeyPair(KeyType::secp256k1);
return { masterPublic, signingKeys.first, makeManifestString (
masterPublic, secret, signingKeys.first, signingKeys.second, 1) };
}
void void
testConfigLoad () testConfigLoad ()
{ {
@@ -113,7 +69,7 @@ private:
Env env (*this); Env env (*this);
auto trustedSites = std::make_unique<ValidatorSite> ( auto trustedSites = std::make_unique<ValidatorSite> (
env.app().getIOService(), env.app().validators(), env.journal); env.app(), env.journal);
// load should accept empty sites list // load should accept empty sites list
std::vector<std::string> emptyCfgSites; std::vector<std::string> emptyCfgSites;
@@ -171,28 +127,11 @@ private:
#endif #endif
} }
class TestSink : public beast::Journal::Sink
{
public:
std::stringstream strm_;
TestSink () : Sink (beast::severities::kDebug, false) { }
void
write (beast::severities::Severity level,
std::string const& text) override
{
if (level < threshold())
return;
strm_ << text << std::endl;
}
};
struct FetchListConfig struct FetchListConfig
{ {
std::string path; std::string path;
std::string msg; std::string msg;
bool ssl;
bool failFetch = false; bool failFetch = false;
bool failApply = false; bool failApply = false;
int serverVersion = 1; int serverVersion = 1;
@@ -205,14 +144,15 @@ private:
testcase << "Fetch list - " << testcase << "Fetch list - " <<
boost::algorithm::join (paths | boost::algorithm::join (paths |
boost::adaptors::transformed( boost::adaptors::transformed(
[](FetchListConfig const& cfg){ return cfg.path; }), [](FetchListConfig const& cfg){
return cfg.path + (cfg.ssl ? " [https]" : " [http]");}),
", "); ", ");
using namespace jtx; using namespace jtx;
Env env (*this); Env env (*this);
auto& trustedKeys = env.app ().validators (); auto& trustedKeys = env.app ().validators ();
TestSink sink; test::StreamSink sink;
beast::Journal journal{sink}; beast::Journal journal{sink};
PublicKey emptyLocalKey; PublicKey emptyLocalKey;
@@ -228,40 +168,28 @@ private:
}; };
std::vector<publisher> servers; std::vector<publisher> servers;
auto const sequence = 1;
auto constexpr listSize = 20; auto constexpr listSize = 20;
std::vector<std::string> cfgPublishers; std::vector<std::string> cfgPublishers;
for (auto const& cfg : paths) for (auto const& cfg : paths)
{ {
auto const publisherSecret = randomSecretKey();
auto const publisherPublic =
derivePublicKey(KeyType::ed25519, publisherSecret);
auto const pubSigningKeys = randomKeyPair(KeyType::secp256k1);
cfgPublishers.push_back(strHex(publisherPublic));
auto const manifest = makeManifestString (
publisherPublic, publisherSecret,
pubSigningKeys.first, pubSigningKeys.second, 1);
servers.push_back(cfg); servers.push_back(cfg);
auto& item = servers.back(); auto& item = servers.back();
item.isRetry = cfg.path == "/bad-resource"; item.isRetry = cfg.path == "/bad-resource";
item.list.reserve (listSize); item.list.reserve (listSize);
while (item.list.size () < listSize) while (item.list.size () < listSize)
item.list.push_back (randomValidator()); item.list.push_back (TrustedPublisherServer::randomValidator());
item.server = std::make_unique<TrustedPublisherServer> ( item.server = std::make_unique<TrustedPublisherServer> (
env.app().getIOService(), env.app().getIOService(),
pubSigningKeys, item.list,
manifest,
sequence,
env.timeKeeper().now() + cfg.expiresFromNow, env.timeKeeper().now() + cfg.expiresFromNow,
cfg.serverVersion, cfg.ssl,
item.list); cfg.serverVersion);
cfgPublishers.push_back(strHex(item.server->publisherPublic()));
std::stringstream uri; std::stringstream uri;
uri << "http://" << item.server->local_endpoint() << cfg.path; uri << (cfg.ssl ? "https://" : "http://") << item.server->local_endpoint() << cfg.path;
item.uri = uri.str(); item.uri = uri.str();
} }
@@ -270,10 +198,7 @@ private:
using namespace std::chrono_literals; using namespace std::chrono_literals;
auto sites = std::make_unique<ValidatorSite> ( auto sites = std::make_unique<ValidatorSite> (
env.app().getIOService(), env.app(), journal, 2s);
env.app().validators(),
journal,
2s);
std::vector<std::string> uris; std::vector<std::string> uris;
for (auto const& u : servers) for (auto const& u : servers)
@@ -300,16 +225,15 @@ private:
BEAST_EXPECTS( BEAST_EXPECTS(
myStatus[jss::last_refresh_message].asString().empty() myStatus[jss::last_refresh_message].asString().empty()
!= u.cfg.failFetch, != u.cfg.failFetch,
to_string(myStatus) + "\n" + sink.strm_.str()); to_string(myStatus) + "\n" + sink.messages().str());
if (! u.cfg.msg.empty()) if (! u.cfg.msg.empty())
{ {
BEAST_EXPECTS( BEAST_EXPECTS(
sink.strm_.str().find(u.cfg.msg) != std::string::npos, sink.messages().str().find(u.cfg.msg) != std::string::npos,
sink.strm_.str()); sink.messages().str());
} }
if (u.cfg.expectedRefreshMin) if (u.cfg.expectedRefreshMin)
{ {
BEAST_EXPECTS( BEAST_EXPECTS(
@@ -347,7 +271,7 @@ private:
Env env (*this); Env env (*this);
TestSink sink; test::StreamSink sink;
beast::Journal journal{sink}; beast::Journal journal{sink};
struct publisher struct publisher
@@ -370,8 +294,7 @@ private:
item.uri = uri.str(); item.uri = uri.str();
} }
auto sites = std::make_unique<ValidatorSite> ( auto sites = std::make_unique<ValidatorSite> (env.app(), journal);
env.app().getIOService(), env.app().validators(), journal);
std::vector<std::string> uris; std::vector<std::string> uris;
for (auto const& u : servers) for (auto const& u : servers)
@@ -393,8 +316,8 @@ private:
if (u.shouldFail) if (u.shouldFail)
{ {
BEAST_EXPECTS( BEAST_EXPECTS(
sink.strm_.str().find(u.expectMsg) != std::string::npos, sink.messages().str().find(u.expectMsg) != std::string::npos,
sink.strm_.str()); sink.messages().str());
log << " -- Msg: " << log << " -- Msg: " <<
myStatus[jss::last_refresh_message].asString() << std::endl; myStatus[jss::last_refresh_message].asString() << std::endl;
} }
@@ -439,71 +362,81 @@ public:
{ {
testConfigLoad (); testConfigLoad ();
for (auto ssl : {true, false})
{
// fetch single site // fetch single site
testFetchList ({{"/validators", ""}}); testFetchList ({{"/validators", "", ssl}});
// fetch multiple sites // fetch multiple sites
testFetchList ({{"/validators", ""}, {"/validators", ""}}); testFetchList ({{"/validators", "", ssl}, {"/validators", "", ssl}});
// fetch single site with single redirects // fetch single site with single redirects
testFetchList ({{"/redirect_once/301", ""}}); testFetchList ({{"/redirect_once/301", "", ssl}});
testFetchList ({{"/redirect_once/302", ""}}); testFetchList ({{"/redirect_once/302", "", ssl}});
testFetchList ({{"/redirect_once/307", ""}}); testFetchList ({{"/redirect_once/307", "", ssl}});
testFetchList ({{"/redirect_once/308", ""}}); testFetchList ({{"/redirect_once/308", "", ssl}});
// one redirect, one not // one redirect, one not
testFetchList ({{"/validators", ""}, {"/redirect_once/302", ""}}); testFetchList ({
{"/validators", "", ssl},
{"/redirect_once/302", "", ssl}});
// fetch single site with undending redirect (fails to load) // fetch single site with undending redirect (fails to load)
testFetchList ({ testFetchList ({
{"/redirect_forever/301", "Exceeded max redirects", true, true}}); {"/redirect_forever/301", "Exceeded max redirects", ssl, true, true}});
// two that redirect forever // two that redirect forever
testFetchList ({ testFetchList ({
{"/redirect_forever/307", "Exceeded max redirects", true, true}, {"/redirect_forever/307", "Exceeded max redirects", ssl, true, true},
{"/redirect_forever/308", "Exceeded max redirects", true, true}}); {"/redirect_forever/308", "Exceeded max redirects", ssl, true, true}});
// one undending redirect, one not // one undending redirect, one not
testFetchList ( testFetchList (
{{"/validators", ""}, {{"/validators", "", ssl},
{"/redirect_forever/302", "Exceeded max redirects", true, true}}); {"/redirect_forever/302", "Exceeded max redirects", ssl, true, true}});
// invalid redir Location // invalid redir Location
testFetchList ({ testFetchList ({
{"/redirect_to/ftp://invalid-url/302", {"/redirect_to/ftp://invalid-url/302",
"Invalid redirect location", "Invalid redirect location",
ssl,
true, true,
true}}); true}});
testFetchList ({ testFetchList ({
{"/redirect_to/file://invalid-url/302", {"/redirect_to/file://invalid-url/302",
"Invalid redirect location", "Invalid redirect location",
ssl,
true, true,
true}}); true}});
// invalid json // invalid json
testFetchList ({ testFetchList ({
{"/validators/bad", "Unable to parse JSON response", true, true}}); {"/validators/bad", "Unable to parse JSON response", ssl, true, true}});
// error status returned // error status returned
testFetchList ({ testFetchList ({
{"/bad-resource", "returned bad status", true, true}}); {"/bad-resource", "returned bad status", ssl, true, true}});
// location field missing // location field missing
testFetchList ({ testFetchList ({
{"/redirect_nolo/308", {"/redirect_nolo/308",
"returned a redirect with no Location", "returned a redirect with no Location",
ssl,
true, true,
true}}); true}});
// json fields missing // json fields missing
testFetchList ({ testFetchList ({
{"/validators/missing", {"/validators/missing",
"Missing fields in JSON response", "Missing fields in JSON response",
ssl,
true, true,
true}}); true}});
// timeout // timeout
testFetchList ({ testFetchList ({
{"/sleep/3", "took too long", true, true}}); {"/sleep/3", "took too long", ssl, true, true}});
// bad manifest version // bad manifest version
testFetchList ({ testFetchList ({
{"/validators", "Unsupported version", false, true, 4}}); {"/validators", "Unsupported version", ssl, false, true, 4}});
using namespace std::chrono_literals; using namespace std::chrono_literals;
// get old validator list // get old validator list
testFetchList ({ testFetchList ({
{"/validators", "Stale validator list", false, true, 1, 0s}}); {"/validators", "Stale validator list", ssl, false, true, 1, 0s}});
// force an out-of-range expiration value // force an out-of-range expiration value
testFetchList ({ testFetchList ({
{"/validators", {"/validators",
"Invalid validator list", "Invalid validator list",
ssl,
false, false,
true, true,
1, 1,
@@ -512,6 +445,7 @@ public:
testFetchList ({ testFetchList ({
{"/validators/refresh/0", {"/validators/refresh/0",
"", "",
ssl,
false, false,
false, false,
1, 1,
@@ -520,6 +454,7 @@ public:
testFetchList ({ testFetchList ({
{"/validators/refresh/10", {"/validators/refresh/10",
"", "",
ssl,
false, false,
false, false,
1, 1,
@@ -528,11 +463,13 @@ public:
testFetchList ({ testFetchList ({
{"/validators/refresh/2000", {"/validators/refresh/2000",
"", "",
ssl,
false, false,
false, false,
1, 1,
detail::default_expires, detail::default_expires,
60*24}}); // max of 24 hours 60*24}}); // max of 24 hours
}
testFileURLs(); testFileURLs();
} }
}; };

View File

@@ -23,11 +23,16 @@
#include <ripple/protocol/SecretKey.h> #include <ripple/protocol/SecretKey.h>
#include <ripple/protocol/Sign.h> #include <ripple/protocol/Sign.h>
#include <ripple/basics/base64.h> #include <ripple/basics/base64.h>
#include <ripple/basics/random.h>
#include <ripple/basics/strHex.h> #include <ripple/basics/strHex.h>
#include <test/jtx/envconfig.h> #include <test/jtx/envconfig.h>
#include <boost/asio.hpp>
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <boost/asio.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/beast/http.hpp> #include <boost/beast/http.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/version.hpp>
#include <boost/lexical_cast.hpp> #include <boost/lexical_cast.hpp>
#include <thread> #include <thread>
@@ -45,11 +50,47 @@ class TrustedPublisherServer
using error_code = boost::system::error_code; using error_code = boost::system::error_code;
socket_type sock_; socket_type sock_;
endpoint_type ep_;
boost::asio::ip::tcp::acceptor acceptor_; boost::asio::ip::tcp::acceptor acceptor_;
std::function<std::string(int)> getList_; std::function<std::string(int)> getList_;
public: // The SSL context is required, and holds certificates
bool useSSL_;
boost::asio::ssl::context sslCtx_{boost::asio::ssl::context::tlsv12};
SecretKey publisherSecret_;
PublicKey publisherPublic_;
// Load a signed certificate into the ssl context, and configure
// the context for use with a server.
inline
void
load_server_certificate()
{
sslCtx_.set_password_callback(
[](std::size_t,
boost::asio::ssl::context_base::password_purpose)
{
return "test";
});
sslCtx_.set_options(
boost::asio::ssl::context::default_workarounds |
boost::asio::ssl::context::no_sslv2 |
boost::asio::ssl::context::single_dh_use);
sslCtx_.use_certificate_chain(
boost::asio::buffer(cert().data(), cert().size()));
sslCtx_.use_private_key(
boost::asio::buffer(key().data(), key().size()),
boost::asio::ssl::context::file_format::pem);
sslCtx_.use_tmp_dh(
boost::asio::buffer(dh().data(), dh().size()));
}
public:
struct Validator struct Validator
{ {
PublicKey masterPublic; PublicKey masterPublic;
@@ -57,19 +98,69 @@ public:
std::string manifest; std::string manifest;
}; };
static
std::string
makeManifestString (
PublicKey const& pk,
SecretKey const& sk,
PublicKey const& spk,
SecretKey const& ssk,
int seq)
{
STObject st(sfGeneric);
st[sfSequence] = seq;
st[sfPublicKey] = pk;
st[sfSigningPubKey] = spk;
sign(st, HashPrefix::manifest, *publicKeyType(spk), ssk);
sign(st, HashPrefix::manifest, *publicKeyType(pk), sk,
sfMasterSignature);
Serializer s;
st.add(s);
return base64_encode (std::string(
static_cast<char const*> (s.data()), s.size()));
}
static
Validator
randomValidator ()
{
auto const secret = randomSecretKey();
auto const masterPublic =
derivePublicKey(KeyType::ed25519, secret);
auto const signingKeys = randomKeyPair(KeyType::secp256k1);
return { masterPublic, signingKeys.first, makeManifestString (
masterPublic, secret, signingKeys.first, signingKeys.second, 1) };
}
TrustedPublisherServer( TrustedPublisherServer(
boost::asio::io_context& ioc, boost::asio::io_context& ioc,
std::pair<PublicKey, SecretKey> keys, std::vector<Validator> const& validators,
std::string const& manifest,
int sequence,
NetClock::time_point expiration, NetClock::time_point expiration,
int version, bool useSSL = false,
std::vector<Validator> const& validators) int version = 1,
: sock_(ioc), acceptor_(ioc) bool immediateStart = true,
int sequence = 1)
: sock_{ioc}
, ep_{beast::IP::Address::from_string(
ripple::test::getEnvLocalhostAddr()),
// 0 means let OS pick the port based on what's available
0}
, acceptor_{ioc}
, useSSL_{useSSL}
, publisherSecret_{randomSecretKey()}
, publisherPublic_{derivePublicKey(KeyType::ed25519, publisherSecret_)}
{ {
endpoint_type const& ep { auto const keys = randomKeyPair(KeyType::secp256k1);
beast::IP::Address::from_string (ripple::test::getEnvLocalhostAddr()), auto const manifest = makeManifestString (
0}; // 0 means let OS pick the port based on what's available publisherPublic_,
publisherSecret_,
keys.first,
keys.second,
1);
std::string data = "{\"sequence\":" + std::to_string(sequence) + std::string data = "{\"sequence\":" + std::to_string(sequence) +
",\"expiration\":" + ",\"expiration\":" +
std::to_string(expiration.time_since_epoch().count()) + std::to_string(expiration.time_since_epoch().count()) +
@@ -94,11 +185,23 @@ public:
return l.str(); return l.str();
}; };
acceptor_.open(ep.protocol()); if (useSSL_)
{
// This holds the self-signed certificate used by the server
load_server_certificate();
}
if (immediateStart)
start();
}
void start()
{
error_code ec; error_code ec;
acceptor_.open(ep_.protocol());
acceptor_.set_option( acceptor_.set_option(
boost::asio::ip::tcp::acceptor::reuse_address(true), ec); boost::asio::ip::tcp::acceptor::reuse_address(true), ec);
acceptor_.bind(ep); acceptor_.bind(ep_);
acceptor_.listen(boost::asio::socket_base::max_connections); acceptor_.listen(boost::asio::socket_base::max_connections);
acceptor_.async_accept( acceptor_.async_accept(
sock_, sock_,
@@ -106,10 +209,17 @@ public:
&TrustedPublisherServer::on_accept, this, std::placeholders::_1)); &TrustedPublisherServer::on_accept, this, std::placeholders::_1));
} }
~TrustedPublisherServer() void stop()
{ {
error_code ec; error_code ec;
acceptor_.close(ec); acceptor_.close(ec);
// TODO consider making this join
// any running do_peer threads
}
~TrustedPublisherServer()
{
stop();
} }
endpoint_type endpoint_type
@@ -118,6 +228,186 @@ public:
return acceptor_.local_endpoint(); return acceptor_.local_endpoint();
} }
PublicKey const&
publisherPublic() const
{
return publisherPublic_;
}
/* CA/self-signed certs :
*
* The following three methods return certs/keys used by
* server and/or client to do the SSL handshake. These strings
* were generated using the script below. The server key and cert
* are used to configure the server (see load_server_certificate
* above). The ca.crt should be used to configure the client
* when ssl verification is enabled.
*
* note:
* cert() ==> server.crt
* key() ==> server.key
* ca_cert() ==> ca.crt
* dh() ==> dh.pem
```
#!/usr/bin/env bash
mkdir -p /tmp/__certs__
pushd /tmp/__certs__
rm *.crt *.key *.pem
# generate CA
openssl genrsa -out ca.key 2048
openssl req -new -x509 -nodes -days 10000 -key ca.key -out ca.crt \
-subj "/C=US/ST=CA/L=Los Angeles/O=rippled-unit-tests/CN=example.com"
# generate private cert
openssl genrsa -out server.key 2048
# Generate certificate signing request
# since our unit tests can run in either ipv4 or ipv6 mode,
# we need to use extensions (subjectAltName) so that we can
# associate both ipv4 and ipv6 localhost addresses with this cert
cat >"extras.cnf" <<EOF
[req]
req_extensions = v3_req
distinguished_name = req_distinguished_name
[req_distinguished_name]
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP.1 = ::1
EOF
openssl req -new -key server.key -out server.csr \
-config extras.cnf \
-subj "/C=US/ST=California/L=San Francisco/O=rippled-unit-tests/CN=127.0.0.1" \
# Create public certificate by signing with our CA
openssl x509 -req -days 10000 -in server.csr -CA ca.crt -CAkey ca.key -out server.crt \
-extfile extras.cnf -set_serial 01 -extensions v3_req
# generate DH params for server
openssl dhparam -out dh.pem 2048
# verify certs
openssl verify -CAfile ca.crt server.crt
openssl x509 -in server.crt -text -noout
popd
```
*/
static
std::string const&
cert()
{
static std::string const cert {R"cert(
-----BEGIN CERTIFICATE-----
MIIDczCCAlugAwIBAgIBATANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEL
MAkGA1UECAwCQ0ExFDASBgNVBAcMC0xvcyBBbmdlbGVzMRswGQYDVQQKDBJyaXBw
bGVkLXVuaXQtdGVzdHMxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE5MDgwNzE3
MzM1OFoXDTQ2MTIyMzE3MzM1OFowazELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNh
bGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xGzAZBgNVBAoMEnJpcHBs
ZWQtdW5pdC10ZXN0czESMBAGA1UEAwwJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEA5Ky0UE9K+gFOznfwBvq2HfnQOOPGtVf4G9m63b5V
QNJYCSNiYxkGZW72ESM3XA8BledlkV9pwIm17+7ucB1Ed3efQjQDq2RSk5LDYDaa
r0Qzzy0EC3b9+AKA6mAoVY6s1Qws/YvM4esz0H+SVvtVcFqA46kRWhJN7M5ic1lu
d58fAq04BHqi5zOEOzfHJYPGUgQOxRTHluYkkkBrL2xioHHnOROshW+PIYFiAc/h
WPzuihPHnKaziPRw+O6O8ysnCxycQHgqtvx73T52eJdLxtr3ToRWaY/8VF/Cog5c
uvWEtg6EucGOszIH8O7eJWaJpVpAfZIX+c62MQWLpOLi/QIDAQABoyowKDAmBgNV
HREEHzAdgglsb2NhbGhvc3SHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQEF
BQADggEBAOhLAO/e0lGi9TZ2HiVi4sJ7KVQaBQHGhfsysILoQNHrNqDypPc/ZrSa
WQ2OqyUeltMnUdN5S1h3MKRZlbAeBQlwkPdjTzlzWkCMWB5BsfIGy5ovqmNQ7zPa
Khg5oxq3mU8ZLiJP4HngyU+hOOCt5tttex2S8ubjFT+3C3cydLKEOXCUPspaVkKn
Eq8WSBoYTvyUVmSi6+m6HGiowWsM5Qgj93IRW6JCbkgfPeKXC/5ykAPQcFHwNaKT
rpWokcavZyMbVjRsbzCQcc7n2j7tbLOu2svSLy6oXwG6n/bEagl5WpN2/TzQuwe7
f5ktutc4DDJSV7fuYYCuGumrHAjcELE=
-----END CERTIFICATE-----
)cert"};
return cert;
}
static
std::string const&
key()
{
static std::string const key {R"pkey(
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA5Ky0UE9K+gFOznfwBvq2HfnQOOPGtVf4G9m63b5VQNJYCSNi
YxkGZW72ESM3XA8BledlkV9pwIm17+7ucB1Ed3efQjQDq2RSk5LDYDaar0Qzzy0E
C3b9+AKA6mAoVY6s1Qws/YvM4esz0H+SVvtVcFqA46kRWhJN7M5ic1lud58fAq04
BHqi5zOEOzfHJYPGUgQOxRTHluYkkkBrL2xioHHnOROshW+PIYFiAc/hWPzuihPH
nKaziPRw+O6O8ysnCxycQHgqtvx73T52eJdLxtr3ToRWaY/8VF/Cog5cuvWEtg6E
ucGOszIH8O7eJWaJpVpAfZIX+c62MQWLpOLi/QIDAQABAoIBACf8mzs/4lh9Sg6I
ooxV4uqy+Fo6WlDzpQsZs7d6xOWk4ogWi+nQQnISSS0N/2w1o41W/UfCa3ejnRDr
sv4f4A0T+eFVvx6FWHs9urRkWAA16OldccufbyGjLm/NiMANRuOqUWO0woru2gyn
git7n6EZ8lfdBI+/i6jRHh4VkV+ROt5Zbp9zuJsj0yMqJH7J6Ebtl1jAF6PemLBL
yxdiYqR8LKTunTGGP/L+4K5a389oPDcJ1+YX805NEopmfrIhPr+BQYdz8905aVFk
FSS4TJy23EhFLzKf3+iSept6Giim+2yy2rv1RPCKgjOXbJ+4LD48xumDol6XWgYr
1CBzQIECgYEA/jBEGOjV02a9A3C5RJxZMawlGwGrvvALG2UrKvwQc595uxwrUw9S
Mn3ZQBEGnEWwEf44jSpWzp8TtejMxEvrU5243eWgwif1kqr1Mcj54DR7Qm15/hsj
M3nA2WscVG2OHBs4AwzMCHE2vfEAkbz71s6xonhg6zvsC26Zy3hYPqkCgYEA5k3k
OuCeG5FXW1/GzhvVFuhl6msNKzuUnLmJg6500XPny5Xo7W3RMvjtTM2XLt1USU6D
arMCCQ1A8ku1SoFdSw5RC6Fl8ZoUFBz9FPPwT6usQssGyFxiiqdHLvTlk12NNCk3
vJYsdQ+v/dKuZ8T4U3GTgQSwPTj6J0kJUf5y2jUCgYEA+hi/R8r/aArz+kiU4T78
O3Vm5NWWCD3ij8fQ23A7N6g3e7RRpF20wF02vmSCHowqmumI9swrsQyvthIiNxmD
pzfORvXCYIY0h2SR77QQt1qr1EYm+6/zyJgI+WL78s4APwNA7y9OKRhLhkN0DfDl
0Qp5mKPcqFbC/tSJmbsFCFECgYEAwlLC2rMgdV5jeWQNGWf+mv+ozu1ZBTuWn88l
qwiO5RSJZwysp3nb5MiJYh6vDAoQznIDDQrSEtUuEcOzypPxJh2EYO3kWMGLY5U6
Lm3OPUs7ZHhu1qytMRUISSS2eWucc4C72NJV3MhJ1T/pjQF0DuRsc5aDJoVm/bLw
vFCYlGkCgYEAgBDIIqdo1th1HE95SQfpP2wV/jA6CPamIciNwS3bpyhDBqs9oLUc
qzXidOpXAVYg1wl/BqpaCQcmmhCrnSLJYdOMpudVyLCCfYmBJ0bs2DCAe5ibGbL7
VruAOjS4yBepkXJU9xwKHxDmgTo/oQ5smq7SNOUWDSElVI/CyZ0x7qA=
-----END RSA PRIVATE KEY-----
)pkey"};
return key;
}
static
std::string const&
ca_cert()
{
static std::string const cert {R"cert(
-----BEGIN CERTIFICATE-----
MIIDQjCCAioCCQDxKQafEvp+VTANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJV
UzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC0xvcyBBbmdlbGVzMRswGQYDVQQKDBJy
aXBwbGVkLXVuaXQtdGVzdHMxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE5MDgw
NzE3MzM1OFoXDTQ2MTIyMzE3MzM1OFowYzELMAkGA1UEBhMCVVMxCzAJBgNVBAgM
AkNBMRQwEgYDVQQHDAtMb3MgQW5nZWxlczEbMBkGA1UECgwScmlwcGxlZC11bml0
LXRlc3RzMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAO9oqh72ttM7hjPnbMcJw0EuyULocEn2hlg4HE4YtzaxlRIz
dHm8nMkG/9yGmHBCuue/Gzssm/CzlduGezae01p8eaFUuEJsjxdrXe89Wk2QH+dm
Fn+SRbGcHaaTV/cyJrvusG7pOu95HL2eebuwiZ+tX5JP01R732iQt8Beeygh/W4P
n2f//fAxbdAIWzx2DH6cmSNe6lpoQe/MN15o8V3whutcC3fkis6wcA7BKZcdVdL2
daFWA6mt4SPWldOfWQVAIX4vRvheWPy34OLCgx+wZWg691Lwd1F+paarKombatUt
vKMTeolFYl3zkZZMYvR0Oyrt5NXUhRfmG7xR3bkCAwEAATANBgkqhkiG9w0BAQsF
AAOCAQEAggKO5WdtU67QPcAdo1Uar0SFouvVLwxJvoKlQ5rqF3idd0HnFVy7iojW
G2sZq7z8SNDMkUXZLbcbYNRyrZI0PdjfI0kyNpaa3pEcPcR8aOcTEOtW6V67FrPG
8aNYpr6a8PPq12aHzPSNjlUGot/qffGIQ0H2OqdWMOUXMMFnmH2KnnWi46Aq3gaF
uyHGrEczjJAK7NTzP8A7fbrmT00Sn6ft1FriQyhvDkUgPXBGWKpOFO84V27oo0ZL
xXQHDWcpX+8yNKynjafkXLx6qXwcySF2bKcTIRsxlN6WNRqZ+wqpNStkjuoFkYR/
IfW9PBfO/gCtNJQ+lqpoTd3kLBCAng==
-----END CERTIFICATE-----
)cert"};
return cert;
}
static
std::string const&
dh()
{
static std::string const dh {R"dh(
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEAnJaaKu3U2a7ZVBvIC+NVNHXo9q6hNCazze+4pwXAKBVXH0ozInEw
WKozYxVJLW7dvDHdjdFOSuTLQDqaPW9zVMQKM0BKu81+JyfJi7C3HYKUw7ECVHp4
DLvhDe6N5eBj/t1FUwcfS2VNIx4QcJiw6FH3CwNNee1fIi5VTRJp2GLUuMCHkT/I
FTODJ+Anw12cJqLdgQfV74UV/Y7JCQl3/DOIy+2YkmX8vWVHX1h6EI5Gw4a3jgqF
gVyCOWoVCfgu37H5e7ERyoAxigiP8hMqoGpmJUYJghVKWoFgNUqXw+guVJ56eIuH
0wVs/LXflOZ42PJAiwv4LTNOtpG2pWGjOwIBAg==
-----END DH PARAMETERS-----
)dh"};
return dh;
}
private: private:
struct lambda struct lambda
{ {
@@ -125,19 +415,25 @@ private:
TrustedPublisherServer& self; TrustedPublisherServer& self;
socket_type sock; socket_type sock;
boost::asio::executor_work_guard<boost::asio::executor> work; boost::asio::executor_work_guard<boost::asio::executor> work;
bool ssl;
lambda(int id_, TrustedPublisherServer& self_, socket_type&& sock_) lambda(
int id_,
TrustedPublisherServer& self_,
socket_type&& sock_,
bool ssl_)
: id(id_) : id(id_)
, self(self_) , self(self_)
, sock(std::move(sock_)) , sock(std::move(sock_))
, work(sock_.get_executor()) , work(sock_.get_executor())
, ssl(ssl_)
{ {
} }
void void
operator()() operator()()
{ {
self.do_peer(id, std::move(sock)); self.do_peer(id, std::move(sock), ssl);
} }
}; };
@@ -150,7 +446,7 @@ private:
return; return;
static int id_ = 0; static int id_ = 0;
std::thread{lambda{++id_, *this, std::move(sock_)}}.detach(); std::thread{lambda{++id_, *this, std::move(sock_), useSSL_}}.detach();
acceptor_.async_accept( acceptor_.async_accept(
sock_, sock_,
std::bind( std::bind(
@@ -158,24 +454,44 @@ private:
} }
void void
do_peer(int id, socket_type&& sock0) do_peer(int id, socket_type&& s, bool ssl)
{ {
using namespace boost::beast; using namespace boost::beast;
socket_type sock(std::move(sock0)); using namespace boost::asio;
multi_buffer sb; socket_type sock(std::move(s));
flat_buffer sb;
error_code ec; error_code ec;
boost::optional<ssl_stream<ip::tcp::socket&>> ssl_stream;
if (ssl)
{
// Construct the stream around the socket
ssl_stream.emplace(sock, sslCtx_);
// Perform the SSL handshake
ssl_stream->handshake(ssl::stream_base::server, ec);
if(ec)
return;
}
for (;;) for (;;)
{ {
resp_type res; resp_type res;
req_type req; req_type req;
try try
{ {
http::read(sock, sb, req, ec); if (ssl)
http::read(*ssl_stream, sb, req, ec);
else
http::read(sock, sb, req, ec);
if (ec) if (ec)
break; break;
auto path = req.target().to_string(); auto path = req.target().to_string();
res.insert("Server", "TrustedPublisherServer"); res.insert("Server", "TrustedPublisherServer");
res.version(req.version()); res.version(req.version());
res.keep_alive(req.keep_alive());
bool prepare = true;
if (boost::starts_with(path, "/validators")) if (boost::starts_with(path, "/validators"))
{ {
@@ -195,6 +511,25 @@ private:
res.body() = getList_(refresh); res.body() = getList_(refresh);
} }
} }
else if (boost::starts_with(path, "/textfile"))
{
prepare = false;
res.result(http::status::ok);
res.insert("Content-Type", "text/example");
// if huge was requested, lie about content length
std::uint64_t cl =
boost::starts_with(path, "/textfile/huge") ?
std::numeric_limits<uint64_t>::max() :
1024;
res.content_length(cl);
if (req.method() == http::verb::get)
{
std::stringstream body;
for (auto i=0; i<1024; ++i)
body << static_cast<char>(rand_int<short>(32, 126)),
res.body() = body.str();
}
}
else if (boost::starts_with(path, "/sleep/")) else if (boost::starts_with(path, "/sleep/"))
{ {
auto const sleep_sec = auto const sleep_sec =
@@ -220,7 +555,8 @@ private:
} }
else if (! boost::starts_with(path, "/redirect_nolo")) else if (! boost::starts_with(path, "/redirect_nolo"))
{ {
location << "http://" << local_endpoint() << location << (ssl ? "https://" : "http://") <<
local_endpoint() <<
(boost::starts_with(path, "/redirect_forever/") ? (boost::starts_with(path, "/redirect_forever/") ?
path : "/validators"); path : "/validators");
} }
@@ -235,7 +571,8 @@ private:
res.body() = "The file '" + path + "' was not found"; res.body() = "The file '" + path + "' was not found";
} }
res.prepare_payload(); if (prepare)
res.prepare_payload();
} }
catch (std::exception const& e) catch (std::exception const& e)
{ {
@@ -247,10 +584,19 @@ private:
res.body() = std::string{"An internal error occurred"} + e.what(); res.body() = std::string{"An internal error occurred"} + e.what();
res.prepare_payload(); res.prepare_payload();
} }
write(sock, res, ec);
if (ec) if (ssl)
write(*ssl_stream, res, ec);
else
write(sock, res, ec);
if (ec || req.need_eof())
break; break;
} }
// Perform the SSL shutdown
if (ssl)
ssl_stream->shutdown(ec);
} }
}; };

View File

@@ -60,6 +60,7 @@ setupConfigForUnitTests (Config& cfg)
cfg["port_ws"].set("admin", getEnvLocalhostAddr()); cfg["port_ws"].set("admin", getEnvLocalhostAddr());
cfg["port_ws"].set("port", port_ws); cfg["port_ws"].set("port", port_ws);
cfg["port_ws"].set("protocol", "ws"); cfg["port_ws"].set("protocol", "ws");
cfg.SSL_VERIFY = false;
} }
namespace jtx { namespace jtx {

View File

@@ -30,25 +30,6 @@ namespace ripple {
class Invariants_test : public beast::unit_test::suite class Invariants_test : public beast::unit_test::suite
{ {
class TestSink : public beast::Journal::Sink
{
public:
std::stringstream strm_;
TestSink () : Sink (beast::severities::kWarning, false) { }
void
write (beast::severities::Severity level,
std::string const& text) override
{
if (level < threshold())
return;
strm_ << text << std::endl;
}
};
// this is common setup/method for running a failing invariant check. The // this is common setup/method for running a failing invariant check. The
// precheck function is used to manipulate the ApplyContext with view // precheck function is used to manipulate the ApplyContext with view
// changes that will cause the check to fail. // changes that will cause the check to fail.
@@ -87,7 +68,7 @@ class Invariants_test : public beast::unit_test::suite
auto tx = STTx {ttACCOUNT_SET, [](STObject&){ } }; auto tx = STTx {ttACCOUNT_SET, [](STObject&){ } };
txmod(tx); txmod(tx);
OpenView ov {*env.current()}; OpenView ov {*env.current()};
TestSink sink; test::StreamSink sink {beast::severities::kWarning};
beast::Journal jlog {sink}; beast::Journal jlog {sink};
ApplyContext ac { ApplyContext ac {
env.app(), env.app(),
@@ -112,20 +93,20 @@ class Invariants_test : public beast::unit_test::suite
? TER {tecINVARIANT_FAILED} ? TER {tecINVARIANT_FAILED}
: TER {tefINVARIANT_FAILED})); : TER {tefINVARIANT_FAILED}));
BEAST_EXPECT( BEAST_EXPECT(
boost::starts_with(sink.strm_.str(), "Invariant failed:") || boost::starts_with(sink.messages().str(), "Invariant failed:") ||
boost::starts_with(sink.strm_.str(), boost::starts_with(sink.messages().str(),
"Transaction caused an exception")); "Transaction caused an exception"));
//uncomment if you want to log the invariant failure message //uncomment if you want to log the invariant failure message
//log << " --> " << sink.strm_.str() << std::endl; //log << " --> " << sink.messages().str() << std::endl;
for (auto const& m : expect_logs) for (auto const& m : expect_logs)
{ {
BEAST_EXPECT(sink.strm_.str().find(m) != std::string::npos); BEAST_EXPECT(sink.messages().str().find(m) != std::string::npos);
} }
} }
else else
{ {
BEAST_EXPECT(tr == tesSUCCESS); BEAST_EXPECT(tr == tesSUCCESS);
BEAST_EXPECT(sink.strm_.str().empty()); BEAST_EXPECT(sink.messages().str().empty());
} }
} }
} }

View File

@@ -0,0 +1,267 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright 2019 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <ripple/net/SSLHTTPDownloader.h>
#include <test/jtx.h>
#include <test/jtx/TrustedPublisherServer.h>
#include <test/unit_test/FileDirGuard.h>
#include <boost/filesystem.hpp>
#include <boost/filesystem/operations.hpp>
#include <mutex>
#include <condition_variable>
namespace ripple {
namespace test {
class SSLHTTPDownloader_test : public beast::unit_test::suite
{
TrustedPublisherServer createServer(
jtx::Env& env,
bool ssl = true)
{
std::vector<TrustedPublisherServer::Validator> list;
list.push_back (TrustedPublisherServer::randomValidator());
return TrustedPublisherServer {
env.app().getIOService(),
list,
env.timeKeeper().now() + std::chrono::seconds{3600},
ssl};
}
struct DownloadCompleter
{
std::mutex m;
std::condition_variable cv;
bool called = false;
boost::filesystem::path dest;
void operator ()(boost::filesystem::path dst)
{
std::unique_lock<std::mutex> lk(m);
called = true;
dest = std::move(dst);
cv.notify_one();
};
bool waitComplete()
{
std::unique_lock<std::mutex> lk(m);
using namespace std::chrono_literals;
#if BOOST_OS_WINDOWS
auto constexpr timeout = 4s;
#else
auto constexpr timeout = 2s;
#endif
auto stat = cv.wait_for(lk, timeout, [this]{return called;});
called = false;
return stat;
};
};
DownloadCompleter cb;
struct Downloader
{
test::StreamSink sink_;
beast::Journal journal_;
// The SSLHTTPDownloader must be created as shared_ptr
// because it uses shared_from_this
std::shared_ptr<SSLHTTPDownloader> ptr_;
Downloader(jtx::Env& env)
: journal_ {sink_}
, ptr_ {std::make_shared<SSLHTTPDownloader>(
env.app().getIOService(), journal_, env.app().config())}
{}
SSLHTTPDownloader* operator->()
{
return ptr_.get();
}
};
void
testDownload(bool verify)
{
testcase <<
std::string("Basic download - SSL ") +
(verify ? "Verify" : "No Verify");
using namespace jtx;
ripple::test::detail::FileDirGuard cert {
*this, "_cert", "ca.pem", TrustedPublisherServer::ca_cert()};
Env env {*this, envconfig([&cert, &verify](std::unique_ptr<Config> cfg)
{
if ((cfg->SSL_VERIFY = verify)) //yes, this is assignment
cfg->SSL_VERIFY_FILE = cert.file().string();
return cfg;
})};
Downloader downloader {env};
// create a TrustedPublisherServer as a simple HTTP
// server to request from. Use the /textfile endpoint
// to get a simple text file sent as response.
auto server = createServer(env);
ripple::test::detail::FileDirGuard const data {
*this, "downloads", "data", "", false, false};
// initiate the download and wait for the callback
// to be invoked
auto stat = downloader->download(
server.local_endpoint().address().to_string(),
std::to_string(server.local_endpoint().port()),
"/textfile",
11,
data.file(),
std::function<void(boost::filesystem::path)> {std::ref(cb)});
if (!BEAST_EXPECT(stat))
{
log << "Failed. LOGS:\n" + downloader.sink_.messages().str();
return;
}
if (!BEAST_EXPECT(cb.waitComplete()))
{
log << "Failed. LOGS:\n" + downloader.sink_.messages().str();
return;
}
BEAST_EXPECT(cb.dest == data.file());
if (!BEAST_EXPECT(boost::filesystem::exists(data.file())))
return;
BEAST_EXPECT(boost::filesystem::file_size(data.file()) > 0);
}
void
testFailures()
{
testcase("Error conditions");
using namespace jtx;
Env env {*this};
{
// file exists
Downloader dl {env};
ripple::test::detail::FileDirGuard const datafile {
*this, "downloads", "data", "file contents"};
BEAST_EXPECT(!dl->download(
"localhost",
"443",
"",
11,
datafile.file(),
std::function<void(boost::filesystem::path)> {std::ref(cb)}));
}
{
// bad hostname
Downloader dl {env};
ripple::test::detail::FileDirGuard const datafile {
*this, "downloads", "data", "", false, false};
BEAST_EXPECT(dl->download(
"badhostname",
"443",
"",
11,
datafile.file(),
std::function<void(boost::filesystem::path)> {std::ref(cb)}));
BEAST_EXPECT(cb.waitComplete());
BEAST_EXPECT(!boost::filesystem::exists(datafile.file()));
BEAST_EXPECTS(
dl.sink_.messages().str().find("async_resolve")
!= std::string::npos,
dl.sink_.messages().str());
}
{
// can't connect
Downloader dl {env};
ripple::test::detail::FileDirGuard const datafile {
*this, "downloads", "data", "", false, false};
auto server = createServer(env);
auto host = server.local_endpoint().address().to_string();
auto port = std::to_string(server.local_endpoint().port());
server.stop();
BEAST_EXPECT(dl->download(
host,
port,
"",
11,
datafile.file(),
std::function<void(boost::filesystem::path)> {std::ref(cb)}));
BEAST_EXPECT(cb.waitComplete());
BEAST_EXPECT(!boost::filesystem::exists(datafile.file()));
BEAST_EXPECTS(
dl.sink_.messages().str().find("async_connect")
!= std::string::npos,
dl.sink_.messages().str());
}
{
// not ssl (failed handlshake)
Downloader dl {env};
ripple::test::detail::FileDirGuard const datafile {
*this, "downloads", "data", "", false, false};
auto server = createServer(env, false);
BEAST_EXPECT(dl->download(
server.local_endpoint().address().to_string(),
std::to_string(server.local_endpoint().port()),
"",
11,
datafile.file(),
std::function<void(boost::filesystem::path)> {std::ref(cb)}));
BEAST_EXPECT(cb.waitComplete());
BEAST_EXPECT(!boost::filesystem::exists(datafile.file()));
BEAST_EXPECTS(
dl.sink_.messages().str().find("async_handshake")
!= std::string::npos,
dl.sink_.messages().str());
}
{
// huge file (content length)
Downloader dl {env};
ripple::test::detail::FileDirGuard const datafile {
*this, "downloads", "data", "", false, false};
auto server = createServer(env);
BEAST_EXPECT(dl->download(
server.local_endpoint().address().to_string(),
std::to_string(server.local_endpoint().port()),
"/textfile/huge",
11,
datafile.file(),
std::function<void(boost::filesystem::path)> {std::ref(cb)}));
BEAST_EXPECT(cb.waitComplete());
BEAST_EXPECT(!boost::filesystem::exists(datafile.file()));
BEAST_EXPECTS(
dl.sink_.messages().str().find("Insufficient disk space")
!= std::string::npos,
dl.sink_.messages().str());
}
}
public:
void
run() override
{
testDownload(true);
testDownload(false);
testFailures();
}
};
BEAST_DEFINE_TESTSUITE(SSLHTTPDownloader, net, ripple);
} // namespace test
} // namespace ripple

View File

@@ -38,47 +38,6 @@ class ValidatorRPC_test : public beast::unit_test::suite
{ {
using Validator = TrustedPublisherServer::Validator; using Validator = TrustedPublisherServer::Validator;
static
Validator
randomValidator ()
{
auto const secret = randomSecretKey();
auto const masterPublic =
derivePublicKey(KeyType::ed25519, secret);
auto const signingKeys = randomKeyPair(KeyType::secp256k1);
return { masterPublic, signingKeys.first, makeManifestString (
masterPublic, secret, signingKeys.first, signingKeys.second, 1) };
}
static
std::string
makeManifestString(
PublicKey const& pk,
SecretKey const& sk,
PublicKey const& spk,
SecretKey const& ssk,
int seq)
{
STObject st(sfGeneric);
st[sfSequence] = seq;
st[sfPublicKey] = pk;
st[sfSigningPubKey] = spk;
sign(st, HashPrefix::manifest, *publicKeyType(spk), ssk);
sign(
st,
HashPrefix::manifest,
*publicKeyType(pk),
sk,
sfMasterSignature);
Serializer s;
st.add(s);
return base64_encode(
std::string(static_cast<char const*>(s.data()), s.size()));
}
public: public:
void void
testPrivileges() testPrivileges()
@@ -184,24 +143,29 @@ public:
return toBase58(TokenType::NodePublic, publicKey); return toBase58(TokenType::NodePublic, publicKey);
}; };
// Publisher manifest/signing keys
auto const publisherSecret = randomSecretKey();
auto const publisherPublic =
derivePublicKey(KeyType::ed25519, publisherSecret);
auto const publisherSigningKeys = randomKeyPair(KeyType::secp256k1);
auto const manifest = makeManifestString(
publisherPublic,
publisherSecret,
publisherSigningKeys.first,
publisherSigningKeys.second,
1);
// Validator keys that will be in the published list // Validator keys that will be in the published list
std::vector<Validator> validators = {randomValidator(), randomValidator()}; std::vector<Validator> validators = {
TrustedPublisherServer::randomValidator(),
TrustedPublisherServer::randomValidator()};
std::set<std::string> expectedKeys; std::set<std::string> expectedKeys;
for (auto const& val : validators) for (auto const& val : validators)
expectedKeys.insert(toStr(val.masterPublic)); expectedKeys.insert(toStr(val.masterPublic));
// Manage single thread io_service for server
struct Worker : BasicApp
{
Worker() : BasicApp(1) {}
};
Worker w;
using namespace std::chrono_literals;
NetClock::time_point const expiration{3600s};
TrustedPublisherServer server{
w.get_io_service(),
validators,
expiration,
false,
1,
false};
//---------------------------------------------------------------------- //----------------------------------------------------------------------
// Publisher list site unavailable // Publisher list site unavailable
@@ -216,7 +180,7 @@ public:
envconfig([&](std::unique_ptr<Config> cfg) { envconfig([&](std::unique_ptr<Config> cfg) {
cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI); cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI);
cfg->section(SECTION_VALIDATOR_LIST_KEYS) cfg->section(SECTION_VALIDATOR_LIST_KEYS)
.append(strHex(publisherPublic)); .append(strHex(server.publisherPublic()));
return cfg; return cfg;
}), }),
}; };
@@ -253,7 +217,7 @@ public:
BEAST_EXPECT(!jp.isMember(jss::expiration)); BEAST_EXPECT(!jp.isMember(jss::expiration));
BEAST_EXPECT(!jp.isMember(jss::version)); BEAST_EXPECT(!jp.isMember(jss::version));
BEAST_EXPECT( BEAST_EXPECT(
jp[jss::pubkey_publisher] == strHex(publisherPublic)); jp[jss::pubkey_publisher] == strHex(server.publisherPublic()));
} }
BEAST_EXPECT(jrr[jss::signing_keys].size() == 0); BEAST_EXPECT(jrr[jss::signing_keys].size() == 0);
} }
@@ -272,24 +236,7 @@ public:
//---------------------------------------------------------------------- //----------------------------------------------------------------------
// Publisher list site available // Publisher list site available
{ {
using namespace std::chrono_literals; server.start();
NetClock::time_point const expiration{3600s};
// Manage single thread io_service for server
struct Worker : BasicApp
{
Worker() : BasicApp(1) {}
};
Worker w;
TrustedPublisherServer server(
w.get_io_service(),
publisherSigningKeys,
manifest,
1,
expiration,
1,
validators);
std::stringstream uri; std::stringstream uri;
uri << "http://" << server.local_endpoint() << "/validators"; uri << "http://" << server.local_endpoint() << "/validators";
@@ -300,7 +247,7 @@ public:
envconfig([&](std::unique_ptr<Config> cfg) { envconfig([&](std::unique_ptr<Config> cfg) {
cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI); cfg->section(SECTION_VALIDATOR_LIST_SITES).append(siteURI);
cfg->section(SECTION_VALIDATOR_LIST_KEYS) cfg->section(SECTION_VALIDATOR_LIST_KEYS)
.append(strHex(publisherPublic)); .append(strHex(server.publisherPublic()));
return cfg; return cfg;
}), }),
}; };
@@ -356,7 +303,7 @@ public:
} }
BEAST_EXPECT(jp[jss::seq].asUInt() == 1); BEAST_EXPECT(jp[jss::seq].asUInt() == 1);
BEAST_EXPECT( BEAST_EXPECT(
jp[jss::pubkey_publisher] == strHex(publisherPublic)); jp[jss::pubkey_publisher] == strHex(server.publisherPublic()));
BEAST_EXPECT(jp[jss::expiration] == to_string(expiration)); BEAST_EXPECT(jp[jss::expiration] == to_string(expiration));
BEAST_EXPECT(jp[jss::version] == 1); BEAST_EXPECT(jp[jss::version] == 1);
} }

View File

@@ -111,18 +111,23 @@ class FileDirGuard : public DirGuard
{ {
protected: protected:
path const file_; path const file_;
bool created_ = false;
public: public:
FileDirGuard(beast::unit_test::suite& test, FileDirGuard(beast::unit_test::suite& test,
path subDir, path file, std::string const& contents, path subDir, path file, std::string const& contents,
bool useCounter = true) bool useCounter = true, bool create = true)
: DirGuard(test, subDir, useCounter) : DirGuard(test, subDir, useCounter)
, file_(file.is_absolute() ? file : subdir() / file) , file_(file.is_absolute() ? file : subdir() / file)
{ {
if (!exists (file_)) if (!exists (file_))
{ {
std::ofstream o (file_.string ()); if (create)
o << contents; {
std::ofstream o (file_.string ());
o << contents;
created_ = true;
}
} }
else else
{ {
@@ -137,11 +142,16 @@ public:
try try
{ {
using namespace boost::filesystem; using namespace boost::filesystem;
if (!exists (file_)) if (exists (file_))
test_.log << "Expected " << file_.string () {
<< " to be an existing file." << std::endl;
else
remove (file_); remove (file_);
}
else
{
if (created_)
test_.log << "Expected " << file_.string ()
<< " to be an existing file." << std::endl;
}
} }
catch (std::exception& e) catch (std::exception& e)
{ {

View File

@@ -94,6 +94,29 @@ public:
operator beast::Journal&() { return journal_; } operator beast::Journal&() { return journal_; }
}; };
// this sink can be used to create a custom journal
// whose log messages will be captured to a stringstream
// that can be later inspected.
class StreamSink : public beast::Journal::Sink
{
std::stringstream strm_;
public:
StreamSink (
beast::severities::Severity threshold = beast::severities::kDebug)
: Sink (threshold, false) { }
void
write (beast::severities::Severity level,
std::string const& text) override
{
if (level < threshold())
return;
strm_ << text << std::endl;
}
std::stringstream const& messages() const { return strm_ ; }
};
} // test } // test
} // ripple } // ripple

View File

@@ -0,0 +1,21 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Copyright (c) 2019 Ripple Labs Inc.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/net/SSLHTTPDownloader_test.cpp>