#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl::test { class TrustedPublisherServer : public std::enable_shared_from_this { using endpoint_type = boost::asio::ip::tcp::endpoint; using address_type = boost::asio::ip::address; using socket_type = boost::asio::ip::tcp::socket; using req_type = boost::beast::http::request; using resp_type = boost::beast::http::response; using error_code = boost::system::error_code; socket_type sock_; endpoint_type ep_; boost::asio::ip::tcp::acceptor acceptor_; // Generates a version 1 validator list, using the int parameter as the // actual version. std::function getList_; // Generates a version 2 validator list, using the int parameter as the // actual version. std::function getList2_; // 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. void loadServerCertificate() { 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())); } struct BlobInfo { BlobInfo(std::string b, std::string s) : blob(std::move(b)), signature(std::move(s)) { } // base-64 encoded JSON containing the validator list. std::string blob; // hex-encoded signature of the blob using the publisher's signing key std::string signature; }; public: struct Validator { PublicKey masterPublic; PublicKey signingPublic; 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; // NOLINTBEGIN(bugprone-unchecked-optional-access) publicKeyType returns value for valid // keys sign(st, HashPrefix::Manifest, *publicKeyType(spk), ssk); sign(st, HashPrefix::Manifest, *publicKeyType(pk), sk, sfMasterSignature); // NOLINTEND(bugprone-unchecked-optional-access) Serializer s; st.add(s); return base64Encode(std::string(static_cast(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 = masterPublic, .signingPublic = signingKeys.first, .manifest = makeManifestString(masterPublic, secret, signingKeys.first, signingKeys.second, 1)}; } // TrustedPublisherServer must be accessed through a shared_ptr. // This constructor is only public so std::make_shared has access. // The function `makeTrustedPublisherServer` should be used to create // instances. // The `futures` member is expected to be structured as // effective / expiration time point pairs for use in version 2 UNLs TrustedPublisherServer( boost::asio::io_context& ioc, std::vector const& validators, NetClock::time_point validUntil, std::vector> const& futures, bool useSSL = false, int version = 1, bool immediateStart = true, int sequence = 1) : sock_{ioc} , ep_{boost::asio::ip::make_address(xrpl::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_)} { auto const keys = randomKeyPair(KeyType::Secp256k1); auto const manifest = makeManifestString(publisherPublic_, publisherSecret_, keys.first, keys.second, 1); std::vector blobInfo; blobInfo.reserve(futures.size() + 1); auto const [data, blob] = [&]() -> std::pair { // Builds the validator list, then encodes it into a blob. std::string data = "{\"sequence\":" + std::to_string(sequence) + ",\"expiration\":" + std::to_string(validUntil.time_since_epoch().count()) + ",\"validators\":["; for (auto const& val : validators) { data += R"({"validation_public_key":")" + strHex(val.masterPublic) + R"(","manifest":")" + val.manifest + "\"},"; } data.pop_back(); data += "]}"; std::string const blob = base64Encode(data); return std::make_pair(data, blob); }(); auto const sig = strHex(sign(keys.first, keys.second, makeSlice(data))); blobInfo.emplace_back(blob, sig); getList_ = [blob = blob, sig, manifest, version](int interval) { // Build the contents of a version 1 format UNL file std::stringstream l; l << R"({"blob":")" << blob << "\"" << R"(,"signature":")" << sig << "\"" << R"(,"manifest":")" << manifest << "\"" << ",\"refresh_interval\": " << interval << ",\"version\":" << version << '}'; return l.str(); }; for (auto const& future : futures) { std::string data = "{\"sequence\":" + std::to_string(++sequence) + ",\"effective\":" + std::to_string(future.first.time_since_epoch().count()) + ",\"expiration\":" + std::to_string(future.second.time_since_epoch().count()) + ",\"validators\":["; // Use the same set of validators for simplicity for (auto const& val : validators) { data += R"({"validation_public_key":")" + strHex(val.masterPublic) + R"(","manifest":")" + val.manifest + "\"},"; } data.pop_back(); data += "]}"; std::string const blob = base64Encode(data); auto const sig = strHex(sign(keys.first, keys.second, makeSlice(data))); blobInfo.emplace_back(blob, sig); } getList2_ = [blobInfo, manifest, version](int interval) { // Build the contents of a version 2 format UNL file // Use `version + 1` to get 2 for most tests, but have // a "bad" version number for tests that provide an override. std::stringstream l; for (auto const& info : blobInfo) { l << R"({"blob":")" << info.blob << "\"" << R"(,"signature":")" << info.signature << "\"},"; } std::string blobs = l.str(); blobs.pop_back(); l.str(std::string()); l << "{\"blobs_v2\": [ " << blobs << R"(],"manifest":")" << manifest << "\"" << ",\"refresh_interval\": " << interval << ",\"version\":" << (version + 1) << '}'; return l.str(); }; if (useSSL_) { // This holds the self-signed certificate used by the server loadServerCertificate(); } } void start() { error_code ec; acceptor_.open(ep_.protocol()); acceptor_.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true), ec); acceptor_.bind(ep_); acceptor_.listen(boost::asio::socket_base::max_listen_connections); acceptor_.async_accept( sock_, [wp = std::weak_ptr{shared_from_this()}](error_code ec) { if (auto p = wp.lock()) { p->onAccept(ec); } }); } void stop() { error_code ec; acceptor_.close(ec); // TODO: consider making this join // any running do_peer threads } ~TrustedPublisherServer() { stop(); } endpoint_type localEndpoint() const { 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 loadServerCertificate * above). The ca.crt should be used to configure the client * when ssl verification is enabled. * * note: * cert() ==> server.crt * key() ==> server.key * caCert() ==> 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=xrpld-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" < work; bool ssl; Lambda(int id, TrustedPublisherServer& self, socket_type&& sock, bool ssl) : id(id), self(self), sock(std::move(sock)), work(this->sock.get_executor()), ssl(ssl) { } void operator()() { self.doPeer(id, std::move(sock), ssl); } }; void onAccept(error_code ec) { if (ec || !acceptor_.is_open()) return; static int nextId = 0; // NOLINT(readability-identifier-naming) std::thread{Lambda{++nextId, *this, std::move(sock_), useSSL_}}.detach(); acceptor_.async_accept( sock_, [wp = std::weak_ptr{shared_from_this()}](error_code ec) { if (auto p = wp.lock()) { p->onAccept(ec); } }); } void doPeer(int id, socket_type&& s, bool ssl) { using namespace boost::beast; using namespace boost::asio; socket_type sock(std::move(s)); flat_buffer sb; error_code ec; std::optional> sslStream; if (ssl) { // Construct the stream around the socket sslStream.emplace(sock, sslCtx_); // Perform the SSL handshake sslStream->handshake(ssl::stream_base::server, ec); if (ec) return; } for (;;) { resp_type res; req_type req; try { if (ssl) { http::read( *sslStream, sb, req, ec); // NOLINT(bugprone-unchecked-optional-access) // ssl_stream emplaced when ssl==true } else { http::read(sock, sb, req, ec); } if (ec) break; std::string_view const path = req.target(); res.insert("Server", "TrustedPublisherServer"); res.version(req.version()); res.keep_alive(req.keep_alive()); bool prepare = true; if (boost::starts_with(path, "/validators2")) { res.result(http::status::ok); res.insert("Content-Type", "application/json"); if (path == "/validators2/bad") { res.body() = "{ 'bad': \"2']"; } else if (path == "/validators2/missing") { res.body() = "{\"version\": 2}"; } else { int refresh = 5; static constexpr char const* kRefreshPrefix = "/validators2/refresh/"; if (boost::starts_with(path, kRefreshPrefix)) { refresh = boost::lexical_cast( path.substr(strlen(kRefreshPrefix))); } res.body() = getList2_(refresh); } } else if (boost::starts_with(path, "/validators")) { res.result(http::status::ok); res.insert("Content-Type", "application/json"); if (path == "/validators/bad") { res.body() = "{ 'bad': \"1']"; } else if (path == "/validators/missing") { res.body() = "{\"version\": 1}"; } else { int refresh = 5; static constexpr char const* kRefreshPrefix = "/validators/refresh/"; if (boost::starts_with(path, kRefreshPrefix)) { refresh = boost::lexical_cast( path.substr(strlen(kRefreshPrefix))); } 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 const cl = boost::starts_with(path, "/textfile/huge") ? std::numeric_limits::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(randInt(32, 126)), res.body() = body.str(); } } } else if (boost::starts_with(path, "/sleep/")) { auto const sleepSec = boost::lexical_cast(path.substr(7)); std::this_thread::sleep_for(std::chrono::seconds(sleepSec)); } else if (boost::starts_with(path, "/redirect")) { if (boost::ends_with(path, "/301")) { res.result(http::status::moved_permanently); } else if (boost::ends_with(path, "/302")) { res.result(http::status::found); } else if (boost::ends_with(path, "/307")) { res.result(http::status::temporary_redirect); } else if (boost::ends_with(path, "/308")) { res.result(http::status::permanent_redirect); } std::stringstream location; if (boost::starts_with(path, "/redirect_to/")) { location << path.substr(13); } else if (!boost::starts_with(path, "/redirect_nolo")) { location << (ssl ? "https://" : "http://") << localEndpoint() << (boost::starts_with(path, "/redirect_forever/") ? path : "/validators"); } if (!location.str().empty()) res.insert("Location", location.str()); } else { // unknown request res.result(boost::beast::http::status::not_found); res.insert("Content-Type", "text/html"); res.body() = "The file '" + std::string(path) + "' was not " "found"; } if (prepare) res.prepare_payload(); } catch (std::exception const& e) { res = {}; res.result(boost::beast::http::status::internal_server_error); res.version(req.version()); res.insert("Server", "TrustedPublisherServer"); res.insert("Content-Type", "text/html"); res.body() = std::string{"An internal error occurred"} + e.what(); res.prepare_payload(); } if (ssl) { write(*sslStream, res, ec); // NOLINT(bugprone-unchecked-optional-access) // ssl_stream emplaced when ssl==true } else { write(sock, res, ec); } if (ec || req.need_eof()) break; } // Perform the SSL shutdown if (ssl) sslStream->shutdown(ec); // NOLINT(bugprone-unchecked-optional-access) ssl_stream // emplaced when ssl==true } }; inline std::shared_ptr makeTrustedPublisherServer( boost::asio::io_context& ioc, std::vector const& validators, NetClock::time_point validUntil, std::vector> const& futures, bool useSSL = false, int version = 1, bool immediateStart = true, int sequence = 1) { auto const r = std::make_shared( ioc, validators, validUntil, futures, useSSL, version, sequence); if (immediateStart) r->start(); return r; } } // namespace xrpl::test