rippled
Loading...
Searching...
No Matches
ValidatorSite.cpp
1#include <xrpld/app/misc/ValidatorList.h>
2#include <xrpld/app/misc/ValidatorSite.h>
3#include <xrpld/app/misc/detail/WorkFile.h>
4#include <xrpld/app/misc/detail/WorkPlain.h>
5#include <xrpld/app/misc/detail/WorkSSL.h>
6
7#include <xrpl/json/json_reader.h>
8#include <xrpl/protocol/digest.h>
9#include <xrpl/protocol/jss.h>
10
11#include <algorithm>
12
13namespace ripple {
14
17unsigned short constexpr max_redirects = 3;
18
20{
21 if (!parseUrl(pUrl, uri))
22 throw std::runtime_error("URI '" + uri + "' cannot be parsed");
23
24 if (pUrl.scheme == "file")
25 {
26 if (!pUrl.domain.empty())
27 throw std::runtime_error("file URI cannot contain a hostname");
28
29#if BOOST_OS_WINDOWS
30 // Paths on Windows need the leading / removed
31 if (pUrl.path[0] == '/')
33#endif
34
35 if (pUrl.path.empty())
36 throw std::runtime_error("file URI must contain a path");
37 }
38 else if (pUrl.scheme == "http")
39 {
40 if (pUrl.domain.empty())
41 throw std::runtime_error("http URI must contain a hostname");
42
43 if (!pUrl.port)
44 pUrl.port = 80;
45 }
46 else if (pUrl.scheme == "https")
47 {
48 if (pUrl.domain.empty())
49 throw std::runtime_error("https URI must contain a hostname");
50
51 if (!pUrl.port)
52 pUrl.port = 443;
53 }
54 else
55 throw std::runtime_error("Unsupported scheme: '" + pUrl.scheme + "'");
56}
57
68
70 Application& app,
73 : app_{app}
74 , j_{j ? *j : app_.logs().journal("ValidatorSite")}
75 , timer_{app_.getIOContext()}
76 , fetching_{false}
77 , pending_{false}
78 , stopping_{false}
79 , requestTimeout_{timeout}
80{
81}
82
84{
86 if (timer_.expiry() > clock_type::time_point{})
87 {
88 if (!stopping_)
89 {
90 lock.unlock();
91 stop();
92 }
93 else
94 {
95 cv_.wait(lock, [&] { return !fetching_; });
96 }
97 }
98}
99
100bool
102{
103 auto const sites = app_.validators().loadLists();
104 return sites.empty() || load(sites, lock_sites);
105}
106
107bool
109{
110 JLOG(j_.debug()) << "Loading configured validator list sites";
111
113
114 return load(siteURIs, lock);
115}
116
117bool
119 std::vector<std::string> const& siteURIs,
120 std::lock_guard<std::mutex> const& lock_sites)
121{
122 // If no sites are provided, act as if a site failed to load.
123 if (siteURIs.empty())
124 {
125 return missingSite(lock_sites);
126 }
127
128 for (auto const& uri : siteURIs)
129 {
130 try
131 {
132 sites_.emplace_back(uri);
133 }
134 catch (std::exception const& e)
135 {
136 JLOG(j_.error())
137 << "Invalid validator site uri: " << uri << ": " << e.what();
138 return false;
139 }
140 }
141
142 JLOG(j_.debug()) << "Loaded " << siteURIs.size() << " sites";
143
144 return true;
145}
146
147void
149{
152 if (timer_.expiry() == clock_type::time_point{})
153 setTimer(l0, l1);
154}
155
156void
158{
160 cv_.wait(lock, [&] { return !pending_; });
161}
162
163void
165{
167 stopping_ = true;
168 // work::cancel() must be called before the
169 // cv wait in order to kick any asio async operations
170 // that might be pending.
171 if (auto sp = work_.lock())
172 sp->cancel();
173 cv_.wait(lock, [&] { return !fetching_; });
174
175 // docs indicate cancel() can throw, but this should be
176 // reconsidered if it changes to noexcept
177 try
178 {
179 timer_.cancel();
180 }
181 catch (boost::system::system_error const&)
182 {
183 }
184 stopping_ = false;
185 pending_ = false;
186 cv_.notify_all();
187}
188
189void
191 std::lock_guard<std::mutex> const& site_lock,
192 std::lock_guard<std::mutex> const& state_lock)
193{
194 auto next = std::min_element(
195 sites_.begin(), sites_.end(), [](Site const& a, Site const& b) {
196 return a.nextRefresh < b.nextRefresh;
197 });
198
199 if (next != sites_.end())
200 {
201 pending_ = next->nextRefresh <= clock_type::now();
202 cv_.notify_all();
203 timer_.expires_at(next->nextRefresh);
204 auto idx = std::distance(sites_.begin(), next);
205 timer_.async_wait([this, idx](boost::system::error_code const& ec) {
206 this->onTimer(idx, ec);
207 });
208 }
209}
210
211void
214 std::size_t siteIdx,
215 std::lock_guard<std::mutex> const& sites_lock)
216{
217 fetching_ = true;
218 sites_[siteIdx].activeResource = resource;
220 auto timeoutCancel = [this]() {
221 std::lock_guard lock_state{state_mutex_};
222 // docs indicate cancel_one() can throw, but this
223 // should be reconsidered if it changes to noexcept
224 try
225 {
226 timer_.cancel_one();
227 }
228 catch (boost::system::system_error const&)
229 {
230 }
231 };
232 auto onFetch = [this, siteIdx, timeoutCancel](
233 error_code const& err,
234 endpoint_type const& endpoint,
235 detail::response_type&& resp) {
236 timeoutCancel();
237 onSiteFetch(err, endpoint, std::move(resp), siteIdx);
238 };
239
240 auto onFetchFile = [this, siteIdx, timeoutCancel](
241 error_code const& err, std::string const& resp) {
242 timeoutCancel();
243 onTextFetch(err, resp, siteIdx);
244 };
245
246 JLOG(j_.debug()) << "Starting request for " << resource->uri;
247
248 if (resource->pUrl.scheme == "https")
249 {
250 // can throw...
252 resource->pUrl.domain,
253 resource->pUrl.path,
254 std::to_string(*resource->pUrl.port),
256 j_,
257 app_.config(),
258 sites_[siteIdx].lastRequestEndpoint,
259 sites_[siteIdx].lastRequestSuccessful,
260 onFetch);
261 }
262 else if (resource->pUrl.scheme == "http")
263 {
265 resource->pUrl.domain,
266 resource->pUrl.path,
267 std::to_string(*resource->pUrl.port),
269 sites_[siteIdx].lastRequestEndpoint,
270 sites_[siteIdx].lastRequestSuccessful,
271 onFetch);
272 }
273 else
274 {
275 BOOST_ASSERT(resource->pUrl.scheme == "file");
277 resource->pUrl.path, app_.getIOContext(), onFetchFile);
278 }
279
280 sites_[siteIdx].lastRequestSuccessful = false;
281 work_ = sp;
282 sp->run();
283 // start a timer for the request, which shouldn't take more
284 // than requestTimeout_ to complete
285 std::lock_guard lock_state{state_mutex_};
286 timer_.expires_after(requestTimeout_);
287 timer_.async_wait([this, siteIdx](boost::system::error_code const& ec) {
288 this->onRequestTimeout(siteIdx, ec);
289 });
290}
291
292void
294{
295 if (ec)
296 return;
297
298 {
299 std::lock_guard lock_site{sites_mutex_};
300 // In some circumstances, both this function and the response
301 // handler (onSiteFetch or onTextFetch) can get queued and
302 // processed. In all observed cases, the response handler
303 // processes a network error. Usually, this function runs first,
304 // but on extremely rare occasions, the response handler can run
305 // first, which will leave activeResource empty.
306 auto const& site = sites_[siteIdx];
307 if (site.activeResource)
308 JLOG(j_.warn()) << "Request for " << site.activeResource->uri
309 << " took too long";
310 else
311 JLOG(j_.error()) << "Request took too long, but a response has "
312 "already been processed";
313 }
314
315 std::lock_guard lock_state{state_mutex_};
316 if (auto sp = work_.lock())
317 sp->cancel();
318}
319
320void
322{
323 if (ec)
324 {
325 // Restart the timer if any errors are encountered, unless the error
326 // is from the wait operation being aborted due to a shutdown request.
327 if (ec != boost::asio::error::operation_aborted)
328 onSiteFetch(ec, {}, detail::response_type{}, siteIdx);
329 return;
330 }
331
332 try
333 {
335 sites_[siteIdx].nextRefresh =
336 clock_type::now() + sites_[siteIdx].refreshInterval;
337 sites_[siteIdx].redirCount = 0;
338 // the WorkSSL client ctor can throw if SSL init fails
339 makeRequest(sites_[siteIdx].startingResource, siteIdx, lock);
340 }
341 catch (std::exception const& ex)
342 {
343 JLOG(j_.error()) << "Exception in " << __func__ << ": " << ex.what();
345 boost::system::error_code{-1, boost::system::generic_category()},
346 {},
348 siteIdx);
349 }
350}
351
352void
354 std::string const& res,
355 std::size_t siteIdx,
356 std::lock_guard<std::mutex> const& sites_lock)
357{
358 Json::Value const body = [&res, siteIdx, this]() {
359 Json::Reader r;
360 Json::Value body;
361 if (!r.parse(res.data(), body))
362 {
363 JLOG(j_.warn()) << "Unable to parse JSON response from "
364 << sites_[siteIdx].activeResource->uri;
365 throw std::runtime_error{"bad json"};
366 }
367 return body;
368 }();
369
370 auto const [valid, version, blobs] = [&body]() {
371 // Check the easy fields first
372 bool valid = body.isObject() && body.isMember(jss::manifest) &&
373 body[jss::manifest].isString() && body.isMember(jss::version) &&
374 body[jss::version].isInt();
375 // Check the version-specific blob & signature fields
376 std::uint32_t version;
378 if (valid)
379 {
380 version = body[jss::version].asUInt();
381 blobs = ValidatorList::parseBlobs(version, body);
382 valid = !blobs.empty();
383 }
384 return std::make_tuple(valid, version, blobs);
385 }();
386
387 if (!valid)
388 {
389 JLOG(j_.warn()) << "Missing fields in JSON response from "
390 << sites_[siteIdx].activeResource->uri;
391 throw std::runtime_error{"missing fields"};
392 }
393
394 auto const manifest = body[jss::manifest].asString();
395 XRPL_ASSERT(
396 version == body[jss::version].asUInt(),
397 "ripple::ValidatorSite::parseJsonResponse : version match");
398 auto const& uri = sites_[siteIdx].activeResource->uri;
399 auto const hash = sha512Half(manifest, blobs, version);
400 auto const applyResult = app_.validators().applyListsAndBroadcast(
401 manifest,
402 version,
403 blobs,
404 uri,
405 hash,
406 app_.overlay(),
408 app_.getOPs());
409
410 sites_[siteIdx].lastRefreshStatus.emplace(
411 Site::Status{clock_type::now(), applyResult.bestDisposition(), ""});
412
413 for (auto const& [disp, count] : applyResult.dispositions)
414 {
415 switch (disp)
416 {
418 JLOG(j_.debug()) << "Applied " << count
419 << " new validator list(s) from " << uri;
420 break;
422 JLOG(j_.debug()) << "Applied " << count
423 << " expired validator list(s) from " << uri;
424 break;
426 JLOG(j_.debug())
427 << "Ignored " << count
428 << " validator list(s) with current sequence from " << uri;
429 break;
431 JLOG(j_.debug()) << "Processed " << count
432 << " future validator list(s) from " << uri;
433 break;
435 JLOG(j_.debug())
436 << "Ignored " << count
437 << " validator list(s) with future known sequence from "
438 << uri;
439 break;
441 JLOG(j_.warn()) << "Ignored " << count
442 << "stale validator list(s) from " << uri;
443 break;
445 JLOG(j_.warn()) << "Ignored " << count
446 << " untrusted validator list(s) from " << uri;
447 break;
449 JLOG(j_.warn()) << "Ignored " << count
450 << " invalid validator list(s) from " << uri;
451 break;
453 JLOG(j_.warn())
454 << "Ignored " << count
455 << " unsupported version validator list(s) from " << uri;
456 break;
457 default:
458 BOOST_ASSERT(false);
459 }
460 }
461
462 if (body.isMember(jss::refresh_interval) &&
463 body[jss::refresh_interval].isNumeric())
464 {
465 using namespace std::chrono_literals;
466 std::chrono::minutes const refresh = std::clamp(
467 std::chrono::minutes{body[jss::refresh_interval].asUInt()},
468 1min,
470 sites_[siteIdx].refreshInterval = refresh;
471 sites_[siteIdx].nextRefresh =
472 clock_type::now() + sites_[siteIdx].refreshInterval;
473 }
474}
475
479 std::size_t siteIdx,
480 std::lock_guard<std::mutex> const& sites_lock)
481{
482 using namespace boost::beast::http;
484 if (res.find(field::location) == res.end() || res[field::location].empty())
485 {
486 JLOG(j_.warn()) << "Request for validator list at "
487 << sites_[siteIdx].activeResource->uri
488 << " returned a redirect with no Location.";
489 throw std::runtime_error{"missing location"};
490 }
491
492 if (sites_[siteIdx].redirCount == max_redirects)
493 {
494 JLOG(j_.warn()) << "Exceeded max redirects for validator list at "
495 << sites_[siteIdx].loadedResource->uri;
496 throw std::runtime_error{"max redirects"};
497 }
498
499 JLOG(j_.debug()) << "Got redirect for validator list from "
500 << sites_[siteIdx].activeResource->uri
501 << " to new location " << res[field::location];
502
503 try
504 {
505 newLocation =
506 std::make_shared<Site::Resource>(std::string(res[field::location]));
507 ++sites_[siteIdx].redirCount;
508 if (newLocation->pUrl.scheme != "http" &&
509 newLocation->pUrl.scheme != "https")
510 throw std::runtime_error(
511 "invalid scheme in redirect " + newLocation->pUrl.scheme);
512 }
513 catch (std::exception const& ex)
514 {
515 JLOG(j_.error()) << "Invalid redirect location: "
516 << res[field::location];
517 throw;
518 }
519 return newLocation;
520}
521
522void
524 boost::system::error_code const& ec,
525 endpoint_type const& endpoint,
527 std::size_t siteIdx)
528{
529 std::lock_guard lock_sites{sites_mutex_};
530 {
531 if (endpoint != endpoint_type{})
532 sites_[siteIdx].lastRequestEndpoint = endpoint;
533 JLOG(j_.debug()) << "Got completion for "
534 << sites_[siteIdx].activeResource->uri << " "
535 << endpoint;
536 auto onError = [&](std::string const& errMsg, bool retry) {
537 sites_[siteIdx].lastRefreshStatus.emplace(Site::Status{
539 if (retry)
540 sites_[siteIdx].nextRefresh =
542
543 // See if there's a copy saved locally from last time we
544 // saw the list.
545 missingSite(lock_sites);
546 };
547 if (ec)
548 {
549 JLOG(j_.warn())
550 << "Problem retrieving from "
551 << sites_[siteIdx].activeResource->uri << " " << endpoint << " "
552 << ec.value() << ":" << ec.message();
553 onError("fetch error", true);
554 }
555 else
556 {
557 try
558 {
559 using namespace boost::beast::http;
560 switch (res.result())
561 {
562 case status::ok:
563 sites_[siteIdx].lastRequestSuccessful = true;
564 parseJsonResponse(res.body(), siteIdx, lock_sites);
565 break;
566 case status::moved_permanently:
567 case status::permanent_redirect:
568 case status::found:
569 case status::temporary_redirect: {
570 auto newLocation =
571 processRedirect(res, siteIdx, lock_sites);
572 XRPL_ASSERT(
573 newLocation,
574 "ripple::ValidatorSite::onSiteFetch : non-null "
575 "validator");
576 // for perm redirects, also update our starting URI
577 if (res.result() == status::moved_permanently ||
578 res.result() == status::permanent_redirect)
579 {
580 sites_[siteIdx].startingResource = newLocation;
581 }
582 makeRequest(newLocation, siteIdx, lock_sites);
583 return; // we are still fetching, so skip
584 // state update/notify below
585 }
586 default: {
587 JLOG(j_.warn())
588 << "Request for validator list at "
589 << sites_[siteIdx].activeResource->uri << " "
590 << endpoint
591 << " returned bad status: " << res.result_int();
592 onError("bad result code", true);
593 }
594 }
595 }
596 catch (std::exception const& ex)
597 {
598 JLOG(j_.error())
599 << "Exception in " << __func__ << ": " << ex.what();
600 onError(ex.what(), false);
601 }
602 }
603 sites_[siteIdx].activeResource.reset();
604 }
605
606 std::lock_guard lock_state{state_mutex_};
607 fetching_ = false;
608 if (!stopping_)
609 setTimer(lock_sites, lock_state);
610 cv_.notify_all();
611}
612
613void
615 boost::system::error_code const& ec,
616 std::string const& res,
617 std::size_t siteIdx)
618{
619 std::lock_guard lock_sites{sites_mutex_};
620 {
621 try
622 {
623 if (ec)
624 {
625 JLOG(j_.warn()) << "Problem retrieving from "
626 << sites_[siteIdx].activeResource->uri << " "
627 << ec.value() << ": " << ec.message();
628 throw std::runtime_error{"fetch error"};
629 }
630
631 sites_[siteIdx].lastRequestSuccessful = true;
632
633 parseJsonResponse(res, siteIdx, lock_sites);
634 }
635 catch (std::exception const& ex)
636 {
637 JLOG(j_.error())
638 << "Exception in " << __func__ << ": " << ex.what();
639 sites_[siteIdx].lastRefreshStatus.emplace(Site::Status{
641 }
642 sites_[siteIdx].activeResource.reset();
643 }
644
645 std::lock_guard lock_state{state_mutex_};
646 fetching_ = false;
647 if (!stopping_)
648 setTimer(lock_sites, lock_state);
649 cv_.notify_all();
650}
651
654{
655 using namespace std::chrono;
656 using Int = Json::Value::Int;
657
659 Json::Value& jSites = (jrr[jss::validator_sites] = Json::arrayValue);
660 {
662 for (Site const& site : sites_)
663 {
666 uri << site.loadedResource->uri;
667 if (site.loadedResource != site.startingResource)
668 uri << " (redirects to " << site.startingResource->uri + ")";
669 v[jss::uri] = uri.str();
670 v[jss::next_refresh_time] = to_string(site.nextRefresh);
671 if (site.lastRefreshStatus)
672 {
673 v[jss::last_refresh_time] =
674 to_string(site.lastRefreshStatus->refreshed);
675 v[jss::last_refresh_status] =
676 to_string(site.lastRefreshStatus->disposition);
677 if (!site.lastRefreshStatus->message.empty())
678 v[jss::last_refresh_message] =
679 site.lastRefreshStatus->message;
680 }
681 v[jss::refresh_interval_min] =
682 static_cast<Int>(site.refreshInterval.count());
683 }
684 }
685 return jrr;
686}
687} // namespace ripple
T clamp(T... args)
Unserialize a JSON document into a Value.
Definition json_reader.h:20
bool parse(std::string const &document, Value &root)
Read a Value from a JSON document.
Represents a JSON value.
Definition json_value.h:131
Value & append(Value const &value)
Append value to array at the end.
bool isString() const
UInt asUInt() const
bool isObject() const
Json::Int Int
Definition json_value.h:139
std::string asString() const
Returns the unquoted string value.
bool isMember(char const *key) const
Return true if the object has a member named key.
bool isNumeric() const
bool isInt() const
Stream error() const
Definition Journal.h:327
Stream debug() const
Definition Journal.h:309
Stream warn() const
Definition Journal.h:321
virtual Config & config()=0
virtual Overlay & overlay()=0
virtual NetworkOPs & getOPs()=0
virtual ValidatorList & validators()=0
virtual HashRouter & getHashRouter()=0
virtual boost::asio::io_context & getIOContext()=0
std::vector< std::string > loadLists()
PublisherListStats applyListsAndBroadcast(std::string const &manifest, std::uint32_t version, std::vector< ValidatorBlobInfo > const &blobs, std::string siteUri, uint256 const &hash, Overlay &overlay, HashRouter &hashRouter, NetworkOPs &networkOPs)
Apply multiple published lists of public keys, then broadcast it to all peers that have not seen it o...
static std::vector< ValidatorBlobInfo > parseBlobs(std::uint32_t version, Json::Value const &body)
Pull the blob/signature/manifest information out of the appropriate Json body fields depending on the...
void start()
Start fetching lists from sites.
std::condition_variable cv_
beast::Journal const j_
std::vector< Site > sites_
boost::asio::ip::tcp::endpoint endpoint_type
void stop()
Stop fetching lists from sites.
Json::Value getJson() const
Return JSON representation of configured validator sites.
bool load(std::vector< std::string > const &siteURIs)
Load configured site URIs.
void onTextFetch(boost::system::error_code const &ec, std::string const &res, std::size_t siteIdx)
Store latest list fetched from anywhere.
std::weak_ptr< detail::Work > work_
std::chrono::seconds const requestTimeout_
void setTimer(std::lock_guard< std::mutex > const &, std::lock_guard< std::mutex > const &)
Queue next site to be fetched lock over site_mutex_ and state_mutex_ required.
ValidatorSite(Application &app, std::optional< beast::Journal > j=std::nullopt, std::chrono::seconds timeout=std::chrono::seconds{20})
std::atomic< bool > stopping_
void join()
Wait for current fetches from sites to complete.
bool missingSite(std::lock_guard< std::mutex > const &)
If no sites are provided, or a site fails to load, get a list of local cache files from the Validator...
std::shared_ptr< Site::Resource > processRedirect(detail::response_type &res, std::size_t siteIdx, std::lock_guard< std::mutex > const &)
Interpret a redirect response.
void parseJsonResponse(std::string const &res, std::size_t siteIdx, std::lock_guard< std::mutex > const &)
Parse json response from validator list site.
void makeRequest(std::shared_ptr< Site::Resource > resource, std::size_t siteIdx, std::lock_guard< std::mutex > const &)
Initiate request to given resource.
void onRequestTimeout(std::size_t siteIdx, error_code const &ec)
request took too long
std::atomic< bool > pending_
boost::system::error_code error_code
boost::asio::basic_waitable_timer< clock_type > timer_
void onTimer(std::size_t siteIdx, error_code const &ec)
Fetch site whose time has come.
void onSiteFetch(boost::system::error_code const &ec, endpoint_type const &endpoint, detail::response_type &&res, std::size_t siteIdx)
Store latest list fetched from site.
std::atomic< bool > fetching_
T data(T... args)
T distance(T... args)
T empty(T... args)
T is_same_v
T make_tuple(T... args)
T min_element(T... args)
@ arrayValue
array value (ordered list)
Definition json_value.h:26
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:27
TER valid(STTx const &tx, ReadView const &view, AccountID const &src, beast::Journal j)
boost::beast::http::response< boost::beast::http::string_body > response_type
Definition Work.h:12
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
@ unsupported_version
List version is not supported.
@ stale
Trusted publisher key, but seq is too old.
@ accepted
List is valid.
@ untrusted
List signed by untrusted publisher key.
@ same_sequence
Same sequence as current list.
@ pending
List will be valid in the future.
@ known_sequence
Future sequence already seen.
@ expired
List is expired, but has the largest non-pending sequence seen so far.
@ invalid
Invalid format or signature.
bool parseUrl(parsedURL &pUrl, std::string const &strUrl)
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:611
@ manifest
Manifest.
unsigned short constexpr max_redirects
sha512_half_hasher::result_type sha512Half(Args const &... args)
Returns the SHA512-Half of a series of objects.
Definition digest.h:205
auto constexpr error_retry_interval
auto constexpr default_refresh_interval
STL namespace.
T size(T... args)
T str(T... args)
std::shared_ptr< Resource > loadedResource
the original uri as loaded from config
std::shared_ptr< Resource > startingResource
the resource to request at <timer> intervals.
endpoint_type lastRequestEndpoint
std::chrono::minutes refreshInterval
clock_type::time_point nextRefresh
std::optional< std::uint16_t > port
T substr(T... args)
T to_string(T... args)
T what(T... args)