diff --git a/src/ripple/app/misc/ValidatorSite.h b/src/ripple/app/misc/ValidatorSite.h index 080e84403..ed7b62f28 100644 --- a/src/ripple/app/misc/ValidatorSite.h +++ b/src/ripple/app/misc/ValidatorSite.h @@ -27,6 +27,7 @@ #include #include #include +#include namespace ripple { @@ -73,10 +74,32 @@ private: { clock_type::time_point refreshed; ListDisposition disposition; + std::string message; }; - std::string uri; - parsedURL pUrl; + struct Resource + { + explicit Resource(std::string uri_); + const std::string uri; + parsedURL pUrl; + }; + + explicit Site(std::string uri); + + /// the original uri as loaded from config + std::shared_ptr loadedResource; + + /// the resource to request at + /// intervals. same as loadedResource + /// except in the case of a permanent redir. + std::shared_ptr startingResource; + + /// the active resource being requested. + /// same as startingResource except + /// when we've gotten a temp redirect + std::shared_ptr activeResource; + + unsigned short redirCount; std::chrono::minutes refreshInterval; clock_type::time_point nextRefresh; boost::optional lastRefreshStatus; @@ -176,6 +199,30 @@ private: boost::system::error_code const& ec, detail::response_type&& res, std::size_t siteIdx); + + /// Initiate request to given resource. + /// lock over sites_mutex_ required + void + makeRequest ( + std::shared_ptr resource, + std::size_t siteIdx, + std::lock_guard& lock); + + /// Parse json response from validator list site. + /// lock over sites_mutex_ required + void + parseJsonResponse ( + detail::response_type& res, + std::size_t siteIdx, + std::lock_guard& lock); + + /// Interpret a redirect response. + /// lock over sites_mutex_ required + std::shared_ptr + processRedirect ( + detail::response_type& res, + std::size_t siteIdx, + std::lock_guard& lock); }; } // ripple diff --git a/src/ripple/app/misc/impl/ValidatorSite.cpp b/src/ripple/app/misc/impl/ValidatorSite.cpp index 7e35a2343..c58fe1deb 100644 --- a/src/ripple/app/misc/impl/ValidatorSite.cpp +++ b/src/ripple/app/misc/impl/ValidatorSite.cpp @@ -31,6 +31,30 @@ namespace ripple { // default site query frequency - 5 minutes auto constexpr DEFAULT_REFRESH_INTERVAL = std::chrono::minutes{5}; +auto constexpr ERROR_RETRY_INTERVAL = std::chrono::seconds{30}; +unsigned short constexpr MAX_REDIRECTS = 3; + +ValidatorSite::Site::Resource::Resource (std::string uri_) + : uri {std::move(uri_)} +{ + if (! parseUrl (pUrl, uri) || + (pUrl.scheme != "http" && pUrl.scheme != "https")) + { + throw std::runtime_error {"invalid url"}; + } + + if (! pUrl.port) + pUrl.port = (pUrl.scheme == "https") ? 443 : 80; +} + +ValidatorSite::Site::Site (std::string uri) + : loadedResource {std::make_shared(std::move(uri))} + , startingResource {loadedResource} + , redirCount {0} + , refreshInterval {DEFAULT_REFRESH_INTERVAL} + , nextRefresh {clock_type::now()} +{ +} ValidatorSite::ValidatorSite ( boost::asio::io_service& ios, @@ -72,22 +96,18 @@ ValidatorSite::load ( std::lock_guard lock{sites_mutex_}; - for (auto uri : siteURIs) + for (auto const& uri : siteURIs) { - parsedURL pUrl; - if (! parseUrl (pUrl, uri) || - (pUrl.scheme != "http" && pUrl.scheme != "https")) + try + { + sites_.emplace_back (uri); + } + catch (std::exception &) { JLOG (j_.error()) << "Invalid validator site uri: " << uri; return false; } - - if (! pUrl.port) - pUrl.port = (pUrl.scheme == "https") ? 443 : 80; - - sites_.push_back ({ - uri, pUrl, DEFAULT_REFRESH_INTERVAL, clock_type::now()}); } JLOG (j_.debug()) << @@ -149,6 +169,45 @@ ValidatorSite::setTimer () } } +void +ValidatorSite::makeRequest ( + std::shared_ptr resource, + std::size_t siteIdx, + std::lock_guard& lock) +{ + fetching_ = true; + sites_[siteIdx].activeResource = resource; + std::shared_ptr sp; + auto onFetch = + [this, siteIdx] (error_code const& err, detail::response_type&& resp) + { + onSiteFetch (err, std::move(resp), siteIdx); + }; + + if (resource->pUrl.scheme == "https") + { + sp = std::make_shared( + resource->pUrl.domain, + resource->pUrl.path, + std::to_string(*resource->pUrl.port), + ios_, + j_, + onFetch); + } + else + { + sp = std::make_shared( + resource->pUrl.domain, + resource->pUrl.path, + std::to_string(*resource->pUrl.port), + ios_, + onFetch); + } + + work_ = sp; + sp->run (); +} + void ValidatorSite::onTimer ( std::size_t siteIdx, @@ -165,40 +224,156 @@ ValidatorSite::onTimer ( std::lock_guard lock{sites_mutex_}; sites_[siteIdx].nextRefresh = - clock_type::now() + DEFAULT_REFRESH_INTERVAL; + clock_type::now() + sites_[siteIdx].refreshInterval; assert(! fetching_); - fetching_ = true; - - std::shared_ptr sp; - if (sites_[siteIdx].pUrl.scheme == "https") + sites_[siteIdx].redirCount = 0; + try { - sp = std::make_shared( - sites_[siteIdx].pUrl.domain, - sites_[siteIdx].pUrl.path, - std::to_string(*sites_[siteIdx].pUrl.port), - ios_, - j_, - [this, siteIdx](error_code const& err, detail::response_type&& resp) - { - onSiteFetch (err, std::move(resp), siteIdx); - }); + // the WorkSSL client can throw if SSL init fails + makeRequest(sites_[siteIdx].startingResource, siteIdx, lock); + } + catch (std::exception &) + { + onSiteFetch( + boost::system::error_code {-1, boost::system::generic_category()}, + detail::response_type {}, + siteIdx); + } +} + +void +ValidatorSite::parseJsonResponse ( + detail::response_type& res, + std::size_t siteIdx, + std::lock_guard& lock) +{ + Json::Reader r; + Json::Value body; + if (! r.parse(res.body().data(), body)) + { + JLOG (j_.warn()) << + "Unable to parse JSON response from " << + sites_[siteIdx].activeResource->uri; + throw std::runtime_error{"bad json"}; + } + + if( ! body.isObject () || + ! body.isMember("blob") || ! body["blob"].isString () || + ! body.isMember("manifest") || ! body["manifest"].isString () || + ! body.isMember("signature") || ! body["signature"].isString() || + ! body.isMember("version") || ! body["version"].isInt()) + { + JLOG (j_.warn()) << + "Missing fields in JSON response from " << + sites_[siteIdx].activeResource->uri; + throw std::runtime_error{"missing fields"}; + } + + auto const disp = validators_.applyList ( + body["manifest"].asString (), + body["blob"].asString (), + body["signature"].asString(), + body["version"].asUInt()); + + sites_[siteIdx].lastRefreshStatus.emplace( + Site::Status{clock_type::now(), disp, ""}); + + if (ListDisposition::accepted == disp) + { + JLOG (j_.debug()) << + "Applied new validator list from " << + sites_[siteIdx].activeResource->uri; + } + else if (ListDisposition::same_sequence == disp) + { + JLOG (j_.debug()) << + "Validator list with current sequence from " << + sites_[siteIdx].activeResource->uri; + } + else if (ListDisposition::stale == disp) + { + JLOG (j_.warn()) << + "Stale validator list from " << + sites_[siteIdx].activeResource->uri; + } + else if (ListDisposition::untrusted == disp) + { + JLOG (j_.warn()) << + "Untrusted validator list from " << + sites_[siteIdx].activeResource->uri; + } + else if (ListDisposition::invalid == disp) + { + JLOG (j_.warn()) << + "Invalid validator list from " << + sites_[siteIdx].activeResource->uri; + } + else if (ListDisposition::unsupported_version == disp) + { + JLOG (j_.warn()) << + "Unsupported version validator list from " << + sites_[siteIdx].activeResource->uri; } else { - sp = std::make_shared( - sites_[siteIdx].pUrl.domain, - sites_[siteIdx].pUrl.path, - std::to_string(*sites_[siteIdx].pUrl.port), - ios_, - [this, siteIdx](error_code const& err, detail::response_type&& resp) - { - onSiteFetch (err, std::move(resp), siteIdx); - }); + BOOST_ASSERT(false); } - work_ = sp; - sp->run (); + if (body.isMember ("refresh_interval") && + body["refresh_interval"].isNumeric ()) + { + // TODO: should we sanity check/clamp this value + // to something reasonable? + sites_[siteIdx].refreshInterval = + std::chrono::minutes{body["refresh_interval"].asUInt ()}; + } +} + +std::shared_ptr +ValidatorSite::processRedirect ( + detail::response_type& res, + std::size_t siteIdx, + std::lock_guard& lock) +{ + using namespace boost::beast::http; + std::shared_ptr newLocation; + if (res.find(field::location) == res.end() || + res[field::location].empty()) + { + JLOG (j_.warn()) << + "Request for validator list at " << + sites_[siteIdx].activeResource->uri << + " returned a redirect with no Location."; + throw std::runtime_error{"missing location"}; + } + + if (sites_[siteIdx].redirCount == MAX_REDIRECTS) + { + JLOG (j_.warn()) << + "Exceeded max redirects for validator list at " << + sites_[siteIdx].loadedResource->uri ; + throw std::runtime_error{"max redirects"}; + } + + JLOG (j_.debug()) << + "Got redirect for validator list from " << + sites_[siteIdx].activeResource->uri << + " to new location " << res[field::location]; + + try + { + newLocation = std::make_shared( + std::string(res[field::location])); + ++sites_[siteIdx].redirCount; + } + catch (std::exception &) + { + JLOG (j_.error()) << + "Invalid redirect location: " << res[field::location]; + throw; + } + return newLocation; } void @@ -207,111 +382,74 @@ ValidatorSite::onSiteFetch( detail::response_type&& res, std::size_t siteIdx) { - if (! ec && res.result() != boost::beast::http::status::ok) + bool shouldRetry = false; { - std::lock_guard lock{sites_mutex_}; - JLOG (j_.warn()) << - "Request for validator list at " << - sites_[siteIdx].uri << " returned " << res.result_int(); - - sites_[siteIdx].lastRefreshStatus.emplace( - Site::Status{clock_type::now(), ListDisposition::invalid}); - } - else if (! ec) - { - std::lock_guard lock{sites_mutex_}; - Json::Reader r; - Json::Value body; - if (r.parse(res.body().data(), body) && - body.isObject () && - body.isMember("blob") && body["blob"].isString () && - body.isMember("manifest") && body["manifest"].isString () && - body.isMember("signature") && body["signature"].isString() && - body.isMember("version") && body["version"].isInt()) + std::lock_guard lock_sites{sites_mutex_}; + try { - - auto const disp = validators_.applyList ( - body["manifest"].asString (), - body["blob"].asString (), - body["signature"].asString(), - body["version"].asUInt()); - - sites_[siteIdx].lastRefreshStatus.emplace( - Site::Status{clock_type::now(), disp}); - - if (ListDisposition::accepted == disp) - { - JLOG (j_.debug()) << - "Applied new validator list from " << - sites_[siteIdx].uri; - } - else if (ListDisposition::same_sequence == disp) - { - JLOG (j_.debug()) << - "Validator list with current sequence from " << - sites_[siteIdx].uri; - } - else if (ListDisposition::stale == disp) + if (ec) { JLOG (j_.warn()) << - "Stale validator list from " << sites_[siteIdx].uri; - } - else if (ListDisposition::untrusted == disp) - { - JLOG (j_.warn()) << - "Untrusted validator list from " << - sites_[siteIdx].uri; - } - else if (ListDisposition::invalid == disp) - { - JLOG (j_.warn()) << - "Invalid validator list from " << - sites_[siteIdx].uri; - } - else if (ListDisposition::unsupported_version == disp) - { - JLOG (j_.warn()) << - "Unsupported version validator list from " << - sites_[siteIdx].uri; - } - else - { - BOOST_ASSERT(false); + "Problem retrieving from " << + sites_[siteIdx].activeResource->uri << + " " << + ec.value() << + ":" << + ec.message(); + shouldRetry = true; + throw std::runtime_error{"fetch error"}; } - if (body.isMember ("refresh_interval") && - body["refresh_interval"].isNumeric ()) + using namespace boost::beast::http; + switch (res.result()) { - sites_[siteIdx].refreshInterval = - std::chrono::minutes{body["refresh_interval"].asUInt ()}; + case status::ok: + parseJsonResponse(res, siteIdx, lock_sites); + break; + case status::moved_permanently : + case status::permanent_redirect : + case status::found : + case status::temporary_redirect : + { + auto newLocation = processRedirect (res, siteIdx, lock_sites); + assert(newLocation); + // for perm redirects, also update our starting URI + if (res.result() == status::moved_permanently || + res.result() == status::permanent_redirect) + { + sites_[siteIdx].startingResource = newLocation; + } + makeRequest(newLocation, siteIdx, lock_sites); + return; // we are still fetching, so skip + // state update/notify below } + default: + { + JLOG (j_.warn()) << + "Request for validator list at " << + sites_[siteIdx].activeResource->uri << + " returned bad status: " << + res.result_int(); + shouldRetry = true; + throw std::runtime_error{"bad result code"}; + } + } + } - else + catch (std::exception& ex) { - JLOG (j_.warn()) << - "Unable to parse JSON response from " << - sites_[siteIdx].uri; - sites_[siteIdx].lastRefreshStatus.emplace( - Site::Status{clock_type::now(), ListDisposition::invalid}); + Site::Status{clock_type::now(), + ListDisposition::invalid, + ex.what()}); + if (shouldRetry) + sites_[siteIdx].nextRefresh = + clock_type::now() + ERROR_RETRY_INTERVAL; } - } - else - { - std::lock_guard lock{sites_mutex_}; - sites_[siteIdx].lastRefreshStatus.emplace( - Site::Status{clock_type::now(), ListDisposition::invalid}); - - JLOG (j_.warn()) << - "Problem retrieving from " << - sites_[siteIdx].uri << - " " << - ec.value() << - ":" << - ec.message(); + sites_[siteIdx].activeResource.reset(); } - std::lock_guard lock{state_mutex_}; + std::lock_guard lock_state{state_mutex_}; fetching_ = false; if (! stopping_) setTimer (); @@ -331,15 +469,22 @@ ValidatorSite::getJson() const for (Site const& site : sites_) { Json::Value& v = jSites.append(Json::objectValue); - v[jss::uri] = site.uri; + std::stringstream uri; + uri << site.loadedResource->uri; + if (site.loadedResource != site.startingResource) + uri << " (redirects to " << site.startingResource->uri + ")"; + v[jss::uri] = uri.str(); + v[jss::next_refresh_time] = to_string(site.nextRefresh); if (site.lastRefreshStatus) { v[jss::last_refresh_time] = to_string(site.lastRefreshStatus->refreshed); v[jss::last_refresh_status] = to_string(site.lastRefreshStatus->disposition); + if (! site.lastRefreshStatus->message.empty()) + v[jss::last_refresh_message] = + site.lastRefreshStatus->message; } - v[jss::refresh_interval_min] = static_cast(site.refreshInterval.count()); } diff --git a/src/ripple/protocol/JsonFields.h b/src/ripple/protocol/JsonFields.h index d9fcecabe..69acbb108 100644 --- a/src/ripple/protocol/JsonFields.h +++ b/src/ripple/protocol/JsonFields.h @@ -233,6 +233,7 @@ JSS ( last ); // out: RPCVersion JSS ( last_close ); // out: NetworkOPs JSS ( last_refresh_time ); // out: ValidatorSite JSS ( last_refresh_status ); // out: ValidatorSite +JSS ( last_refresh_message ); // out: ValidatorSite JSS ( ledger ); // in: NetworkOPs, LedgerCleaner, // RPCHelpers // out: NetworkOPs, PeerImp @@ -305,6 +306,7 @@ JSS ( name ); // out: AmendmentTableImpl, PeerImp JSS ( needed_state_hashes ); // out: InboundLedger JSS ( needed_transaction_hashes ); // out: InboundLedger JSS ( network_ledger ); // out: NetworkOPs +JSS ( next_refresh_time ); // out: ValidatorSite JSS ( no_ripple ); // out: AccountLines JSS ( no_ripple_peer ); // out: AccountLines JSS ( node ); // out: LedgerEntry diff --git a/src/test/app/ValidatorSite_test.cpp b/src/test/app/ValidatorSite_test.cpp index caf410e92..65e5872d6 100644 --- a/src/test/app/ValidatorSite_test.cpp +++ b/src/test/app/ValidatorSite_test.cpp @@ -23,11 +23,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include namespace ripple { @@ -121,121 +123,131 @@ private: BEAST_EXPECT(!trustedSites->load (badSites)); } - void - testFetchList () + class TestSink : public beast::Journal::Sink { - testcase ("Fetch list"); + 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; + } + }; + + void + testFetchList ( + std::vector> const& paths) + { + testcase << "Fetch list - " << paths[0].first << + (paths.size() > 1 ? ", " + paths[1].first : ""); using namespace jtx; Env env (*this); auto& trustedKeys = env.app ().validators (); + TestSink sink; + beast::Journal journal{sink}; + PublicKey emptyLocalKey; std::vector emptyCfgKeys; - - auto const publisherSecret1 = randomSecretKey(); - auto const publisherPublic1 = - derivePublicKey(KeyType::ed25519, publisherSecret1); - auto const pubSigningKeys1 = randomKeyPair(KeyType::secp256k1); - - auto const manifest1 = makeManifestString ( - publisherPublic1, publisherSecret1, - pubSigningKeys1.first, pubSigningKeys1.second, 1); - - auto const publisherSecret2 = randomSecretKey(); - auto const publisherPublic2 = - derivePublicKey(KeyType::ed25519, publisherSecret2); - auto const pubSigningKeys2 = randomKeyPair(KeyType::secp256k1); - - auto const manifest2 = makeManifestString ( - publisherPublic2, publisherSecret2, - pubSigningKeys2.first, pubSigningKeys2.second, 1); - - std::vector cfgPublishers({ - strHex(publisherPublic1), - strHex(publisherPublic2)}); - - BEAST_EXPECT(trustedKeys.load ( - emptyLocalKey, emptyCfgKeys, cfgPublishers)); - - auto constexpr listSize = 20; - std::vector list1; - list1.reserve (listSize); - while (list1.size () < listSize) - list1.push_back (randomValidator()); - - std::vector list2; - list2.reserve (listSize); - while (list2.size () < listSize) - list2.push_back (randomValidator()); + struct publisher + { + std::unique_ptr server; + std::vector list; + std::string uri; + std::string expectMsg; + bool shouldFail; + }; + std::vector servers; auto const sequence = 1; auto const version = 1; using namespace std::chrono_literals; NetClock::time_point const expiration = env.timeKeeper().now() + 3600s; + auto constexpr listSize = 20; + std::vector cfgPublishers; - TrustedPublisherServer server1( - env.app().getIOService(), - pubSigningKeys1, - manifest1, - sequence, - expiration, - version, - list1); - - TrustedPublisherServer server2( - env.app().getIOService(), - pubSigningKeys2, - manifest2, - sequence, - expiration, - version, - list2); - - std::stringstream url1, url2; - url1 << "http://" << server1.local_endpoint() << "/validators"; - url2 << "http://" << server2.local_endpoint() << "/validators"; - + for (auto const& cfg : paths) { - // fetch single site - std::vector cfgSites({ url1.str() }); + auto const publisherSecret = randomSecretKey(); + auto const publisherPublic = + derivePublicKey(KeyType::ed25519, publisherSecret); + auto const pubSigningKeys = randomKeyPair(KeyType::secp256k1); + cfgPublishers.push_back(strHex(publisherPublic)); - auto sites = std::make_unique ( - env.app().getIOService(), env.app().validators(), env.journal); + auto const manifest = makeManifestString ( + publisherPublic, publisherSecret, + pubSigningKeys.first, pubSigningKeys.second, 1); - sites->load (cfgSites); - sites->start(); - sites->join(); + servers.push_back({}); + auto& item = servers.back(); + item.shouldFail = ! cfg.second.empty(); + item.expectMsg = cfg.second; + item.list.reserve (listSize); + while (item.list.size () < listSize) + item.list.push_back (randomValidator()); - for (auto const& val : list1) - { - BEAST_EXPECT(trustedKeys.listed (val.masterPublic)); - BEAST_EXPECT(trustedKeys.listed (val.signingPublic)); - } + item.server = std::make_unique ( + env.app().getIOService(), + pubSigningKeys, + manifest, + sequence, + expiration, + version, + item.list); + + std::stringstream uri; + uri << "http://" << item.server->local_endpoint() << cfg.first; + item.uri = uri.str(); } + + BEAST_EXPECT(trustedKeys.load ( + emptyLocalKey, emptyCfgKeys, cfgPublishers)); + + auto sites = std::make_unique ( + env.app().getIOService(), env.app().validators(), journal); + + std::vector uris; + for (auto const& u : servers) + uris.push_back(u.uri); + sites->load (uris); + sites->start(); + sites->join(); + + for (auto const& u : servers) { - // fetch multiple sites - std::vector cfgSites({ url1.str(), url2.str() }); - - auto sites = std::make_unique ( - env.app().getIOService(), env.app().validators(), env.journal); - - sites->load (cfgSites); - sites->start(); - sites->join(); - - for (auto const& val : list1) + for (auto const& val : u.list) { - BEAST_EXPECT(trustedKeys.listed (val.masterPublic)); - BEAST_EXPECT(trustedKeys.listed (val.signingPublic)); + BEAST_EXPECT( + trustedKeys.listed (val.masterPublic) != u.shouldFail); + BEAST_EXPECT( + trustedKeys.listed (val.signingPublic) != u.shouldFail); } - for (auto const& val : list2) + auto const jv = sites->getJson(); + Json::Value myStatus; + for (auto const& vs : jv[jss::validator_sites]) + if (vs[jss::uri].asString().find(u.uri) != std::string::npos) + myStatus = vs; + BEAST_EXPECTS( + myStatus[jss::last_refresh_message].asString().empty() + != u.shouldFail, to_string(myStatus)); + if (u.shouldFail) { - BEAST_EXPECT(trustedKeys.listed (val.masterPublic)); - BEAST_EXPECT(trustedKeys.listed (val.signingPublic)); + BEAST_EXPECTS( + sink.strm_.str().find(u.expectMsg) != std::string::npos, + sink.strm_.str()); + log << " -- Msg: " << + myStatus[jss::last_refresh_message].asString() << std::endl; } } } @@ -245,7 +257,42 @@ public: run() override { testConfigLoad (); - testFetchList (); + + // fetch single site + testFetchList ({{"/validators",""}}); + // fetch multiple sites + testFetchList ({{"/validators",""}, {"/validators",""}}); + // fetch single site with single redirects + testFetchList ({{"/redirect_once/301",""}}); + testFetchList ({{"/redirect_once/302",""}}); + testFetchList ({{"/redirect_once/307",""}}); + testFetchList ({{"/redirect_once/308",""}}); + // one redirect, one not + testFetchList ({{"/validators",""}, {"/redirect_once/302",""}}); + // fetch single site with undending redirect (fails to load) + testFetchList ({{"/redirect_forever/301", "Exceeded max redirects"}}); + // two that redirect forever + testFetchList ({ + {"/redirect_forever/307","Exceeded max redirects"}, + {"/redirect_forever/308","Exceeded max redirects"}}); + // one undending redirect, one not + testFetchList ( + {{"/validators",""}, + {"/redirect_forever/302","Exceeded max redirects"}}); + // invalid redir Location + testFetchList ({ + {"/redirect_to/ftp://invalid-url/302", + "Invalid redirect location"}}); + // invalid json + testFetchList ({{"/validators/bad", "Unable to parse JSON response"}}); + // error status returned + testFetchList ({{"/bad-resource", "returned bad status"}}); + // location field missing + testFetchList ({ + {"/redirect_nolo/308", "returned a redirect with no Location"}}); + // json fields missing + testFetchList ({ + {"/validators/missing", "Missing fields in JSON response"}}); } }; diff --git a/src/test/jtx/TrustedPublisherServer.h b/src/test/jtx/TrustedPublisherServer.h index 3fb916cda..cd47cbd36 100644 --- a/src/test/jtx/TrustedPublisherServer.h +++ b/src/test/jtx/TrustedPublisherServer.h @@ -26,6 +26,7 @@ #include #include #include +#include #include namespace ripple { @@ -156,36 +157,65 @@ private: void do_peer(int id, socket_type&& sock0) { + using namespace boost::beast; socket_type sock(std::move(sock0)); - boost::beast::multi_buffer sb; + multi_buffer sb; error_code ec; for (;;) { req_type req; - boost::beast::http::read(sock, sb, req, ec); + http::read(sock, sb, req, ec); if (ec) break; auto path = req.target().to_string(); - if (path != "/validators") + resp_type res; + res.insert("Server", "TrustedPublisherServer"); + res.version(req.version()); + + if (boost::starts_with(path, "/validators")) { - resp_type res; + 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 + res.body() = list_; + } + 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 << "http://" << local_endpoint() << + (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.version(req.version()); - res.insert("Server", "TrustedPublisherServer"); res.insert("Content-Type", "text/html"); res.body() = "The file '" + path + "' was not found"; - res.prepare_payload(); - write(sock, res, ec); - if (ec) - break; } - resp_type res; - res.result(boost::beast::http::status::ok); - res.version(req.version()); - res.insert("Server", "TrustedPublisherServer"); - res.insert("Content-Type", "application/json"); - res.body() = list_; try { res.prepare_payload();