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 xrpl {
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 : app_{app}
71 , j_{j ? *j : app_.logs().journal("ValidatorSite")}
72 , timer_{app_.getIOContext()}
73 , fetching_{false}
74 , pending_{false}
75 , stopping_{false}
76 , requestTimeout_{timeout}
77{
78}
79
81{
83 if (timer_.expiry() > clock_type::time_point{})
84 {
85 if (!stopping_)
86 {
87 lock.unlock();
88 stop();
89 }
90 else
91 {
92 cv_.wait(lock, [&] { return !fetching_; });
93 }
94 }
95}
96
97bool
99{
100 auto const sites = app_.validators().loadLists();
101 return sites.empty() || load(sites, lock_sites);
102}
103
104bool
106{
107 JLOG(j_.debug()) << "Loading configured validator list sites";
108
110
111 return load(siteURIs, lock);
112}
113
114bool
116{
117 // If no sites are provided, act as if a site failed to load.
118 if (siteURIs.empty())
119 {
120 return missingSite(lock_sites);
121 }
122
123 for (auto const& uri : siteURIs)
124 {
125 try
126 {
127 sites_.emplace_back(uri);
128 }
129 catch (std::exception const& e)
130 {
131 JLOG(j_.error()) << "Invalid validator site uri: " << uri << ": " << e.what();
132 return false;
133 }
134 }
135
136 JLOG(j_.debug()) << "Loaded " << siteURIs.size() << " sites";
137
138 return true;
139}
140
141void
143{
146 if (timer_.expiry() == clock_type::time_point{})
147 setTimer(l0, l1);
148}
149
150void
152{
154 cv_.wait(lock, [&] { return !pending_; });
155}
156
157void
159{
161 stopping_ = true;
162 // work::cancel() must be called before the
163 // cv wait in order to kick any asio async operations
164 // that might be pending.
165 if (auto sp = work_.lock())
166 sp->cancel();
167 cv_.wait(lock, [&] { return !fetching_; });
168
169 // docs indicate cancel() can throw, but this should be
170 // reconsidered if it changes to noexcept
171 try
172 {
173 timer_.cancel();
174 }
175 catch (boost::system::system_error const&)
176 {
177 }
178 stopping_ = false;
179 pending_ = false;
180 cv_.notify_all();
181}
182
183void
185{
186 auto next = std::min_element(
187 sites_.begin(), sites_.end(), [](Site const& a, Site const& b) { return a.nextRefresh < b.nextRefresh; });
188
189 if (next != sites_.end())
190 {
191 pending_ = next->nextRefresh <= clock_type::now();
192 cv_.notify_all();
193 timer_.expires_at(next->nextRefresh);
194 auto idx = std::distance(sites_.begin(), next);
195 timer_.async_wait([this, idx](boost::system::error_code const& ec) { this->onTimer(idx, ec); });
196 }
197}
198
199void
202 std::size_t siteIdx,
203 std::lock_guard<std::mutex> const& sites_lock)
204{
205 fetching_ = true;
206 sites_[siteIdx].activeResource = resource;
208 auto timeoutCancel = [this]() {
209 std::lock_guard lock_state{state_mutex_};
210 // docs indicate cancel_one() can throw, but this
211 // should be reconsidered if it changes to noexcept
212 try
213 {
214 timer_.cancel_one();
215 }
216 catch (boost::system::system_error const&)
217 {
218 }
219 };
220 auto onFetch = [this, siteIdx, timeoutCancel](
221 error_code const& err, endpoint_type const& endpoint, detail::response_type&& resp) {
222 timeoutCancel();
223 onSiteFetch(err, endpoint, std::move(resp), siteIdx);
224 };
225
226 auto onFetchFile = [this, siteIdx, timeoutCancel](error_code const& err, std::string const& resp) {
227 timeoutCancel();
228 onTextFetch(err, resp, siteIdx);
229 };
230
231 JLOG(j_.debug()) << "Starting request for " << resource->uri;
232
233 if (resource->pUrl.scheme == "https")
234 {
235 // can throw...
237 resource->pUrl.domain,
238 resource->pUrl.path,
239 std::to_string(*resource->pUrl.port),
241 j_,
242 app_.config(),
243 sites_[siteIdx].lastRequestEndpoint,
244 sites_[siteIdx].lastRequestSuccessful,
245 onFetch);
246 }
247 else if (resource->pUrl.scheme == "http")
248 {
250 resource->pUrl.domain,
251 resource->pUrl.path,
252 std::to_string(*resource->pUrl.port),
254 sites_[siteIdx].lastRequestEndpoint,
255 sites_[siteIdx].lastRequestSuccessful,
256 onFetch);
257 }
258 else
259 {
260 BOOST_ASSERT(resource->pUrl.scheme == "file");
261 sp = std::make_shared<detail::WorkFile>(resource->pUrl.path, app_.getIOContext(), onFetchFile);
262 }
263
264 sites_[siteIdx].lastRequestSuccessful = false;
265 work_ = sp;
266 sp->run();
267 // start a timer for the request, which shouldn't take more
268 // than requestTimeout_ to complete
269 std::lock_guard lock_state{state_mutex_};
270 timer_.expires_after(requestTimeout_);
271 timer_.async_wait([this, siteIdx](boost::system::error_code const& ec) { this->onRequestTimeout(siteIdx, ec); });
272}
273
274void
276{
277 if (ec)
278 return;
279
280 {
281 std::lock_guard lock_site{sites_mutex_};
282 // In some circumstances, both this function and the response
283 // handler (onSiteFetch or onTextFetch) can get queued and
284 // processed. In all observed cases, the response handler
285 // processes a network error. Usually, this function runs first,
286 // but on extremely rare occasions, the response handler can run
287 // first, which will leave activeResource empty.
288 auto const& site = sites_[siteIdx];
289 if (site.activeResource)
290 JLOG(j_.warn()) << "Request for " << site.activeResource->uri << " took too long";
291 else
292 JLOG(j_.error()) << "Request took too long, but a response has "
293 "already been processed";
294 }
295
296 std::lock_guard lock_state{state_mutex_};
297 if (auto sp = work_.lock())
298 sp->cancel();
299}
300
301void
303{
304 if (ec)
305 {
306 // Restart the timer if any errors are encountered, unless the error
307 // is from the wait operation being aborted due to a shutdown request.
308 if (ec != boost::asio::error::operation_aborted)
309 onSiteFetch(ec, {}, detail::response_type{}, siteIdx);
310 return;
311 }
312
313 try
314 {
316 sites_[siteIdx].nextRefresh = clock_type::now() + sites_[siteIdx].refreshInterval;
317 sites_[siteIdx].redirCount = 0;
318 // the WorkSSL client ctor can throw if SSL init fails
319 makeRequest(sites_[siteIdx].startingResource, siteIdx, lock);
320 }
321 catch (std::exception const& ex)
322 {
323 JLOG(j_.error()) << "Exception in " << __func__ << ": " << ex.what();
325 boost::system::error_code{-1, boost::system::generic_category()}, {}, detail::response_type{}, siteIdx);
326 }
327}
328
329void
331 std::string const& res,
332 std::size_t siteIdx,
333 std::lock_guard<std::mutex> const& sites_lock)
334{
335 Json::Value const body = [&res, siteIdx, this]() {
336 Json::Reader r;
337 Json::Value body;
338 if (!r.parse(res.data(), body))
339 {
340 JLOG(j_.warn()) << "Unable to parse JSON response from " << sites_[siteIdx].activeResource->uri;
341 throw std::runtime_error{"bad json"};
342 }
343 return body;
344 }();
345
346 auto const [valid, version, blobs] = [&body]() {
347 // Check the easy fields first
348 bool valid = body.isObject() && body.isMember(jss::manifest) && body[jss::manifest].isString() &&
349 body.isMember(jss::version) && body[jss::version].isInt();
350 // Check the version-specific blob & signature fields
351 std::uint32_t version;
353 if (valid)
354 {
355 version = body[jss::version].asUInt();
356 blobs = ValidatorList::parseBlobs(version, body);
357 valid = !blobs.empty();
358 }
359 return std::make_tuple(valid, version, blobs);
360 }();
361
362 if (!valid)
363 {
364 JLOG(j_.warn()) << "Missing fields in JSON response from " << sites_[siteIdx].activeResource->uri;
365 throw std::runtime_error{"missing fields"};
366 }
367
368 auto const manifest = body[jss::manifest].asString();
369 XRPL_ASSERT(version == body[jss::version].asUInt(), "xrpl::ValidatorSite::parseJsonResponse : version match");
370 auto const& uri = sites_[siteIdx].activeResource->uri;
371 auto const hash = sha512Half(manifest, blobs, version);
372 auto const applyResult = app_.validators().applyListsAndBroadcast(
373 manifest, version, blobs, uri, hash, app_.overlay(), app_.getHashRouter(), app_.getOPs());
374
375 sites_[siteIdx].lastRefreshStatus.emplace(Site::Status{clock_type::now(), applyResult.bestDisposition(), ""});
376
377 for (auto const& [disp, count] : applyResult.dispositions)
378 {
379 switch (disp)
380 {
382 JLOG(j_.debug()) << "Applied " << count << " new validator list(s) from " << uri;
383 break;
385 JLOG(j_.debug()) << "Applied " << count << " expired validator list(s) from " << uri;
386 break;
388 JLOG(j_.debug()) << "Ignored " << count << " validator list(s) with current sequence from " << uri;
389 break;
391 JLOG(j_.debug()) << "Processed " << count << " future validator list(s) from " << uri;
392 break;
394 JLOG(j_.debug()) << "Ignored " << count << " validator list(s) with future known sequence from " << uri;
395 break;
397 JLOG(j_.warn()) << "Ignored " << count << "stale validator list(s) from " << uri;
398 break;
400 JLOG(j_.warn()) << "Ignored " << count << " untrusted validator list(s) from " << uri;
401 break;
403 JLOG(j_.warn()) << "Ignored " << count << " invalid validator list(s) from " << uri;
404 break;
406 JLOG(j_.warn()) << "Ignored " << count << " unsupported version validator list(s) from " << uri;
407 break;
408 default:
409 BOOST_ASSERT(false);
410 }
411 }
412
413 if (body.isMember(jss::refresh_interval) && body[jss::refresh_interval].isNumeric())
414 {
415 using namespace std::chrono_literals;
416 std::chrono::minutes const refresh =
417 std::clamp(std::chrono::minutes{body[jss::refresh_interval].asUInt()}, 1min, std::chrono::minutes{24h});
418 sites_[siteIdx].refreshInterval = refresh;
419 sites_[siteIdx].nextRefresh = clock_type::now() + sites_[siteIdx].refreshInterval;
420 }
421}
422
426 std::size_t siteIdx,
427 std::lock_guard<std::mutex> const& sites_lock)
428{
429 using namespace boost::beast::http;
431 if (res.find(field::location) == res.end() || res[field::location].empty())
432 {
433 JLOG(j_.warn()) << "Request for validator list at " << sites_[siteIdx].activeResource->uri
434 << " returned a redirect with no Location.";
435 throw std::runtime_error{"missing location"};
436 }
437
438 if (sites_[siteIdx].redirCount == max_redirects)
439 {
440 JLOG(j_.warn()) << "Exceeded max redirects for validator list at " << sites_[siteIdx].loadedResource->uri;
441 throw std::runtime_error{"max redirects"};
442 }
443
444 JLOG(j_.debug()) << "Got redirect for validator list from " << sites_[siteIdx].activeResource->uri
445 << " to new location " << res[field::location];
446
447 try
448 {
449 newLocation = std::make_shared<Site::Resource>(std::string(res[field::location]));
450 ++sites_[siteIdx].redirCount;
451 if (newLocation->pUrl.scheme != "http" && newLocation->pUrl.scheme != "https")
452 throw std::runtime_error("invalid scheme in redirect " + newLocation->pUrl.scheme);
453 }
454 catch (std::exception const& ex)
455 {
456 JLOG(j_.error()) << "Invalid redirect location: " << res[field::location];
457 throw;
458 }
459 return newLocation;
460}
461
462void
464 boost::system::error_code const& ec,
465 endpoint_type const& endpoint,
467 std::size_t siteIdx)
468{
469 std::lock_guard lock_sites{sites_mutex_};
470 {
471 if (endpoint != endpoint_type{})
472 sites_[siteIdx].lastRequestEndpoint = endpoint;
473 JLOG(j_.debug()) << "Got completion for " << sites_[siteIdx].activeResource->uri << " " << endpoint;
474 auto onError = [&](std::string const& errMsg, bool retry) {
475 sites_[siteIdx].lastRefreshStatus.emplace(
477 if (retry)
478 sites_[siteIdx].nextRefresh = clock_type::now() + error_retry_interval;
479
480 // See if there's a copy saved locally from last time we
481 // saw the list.
482 missingSite(lock_sites);
483 };
484 if (ec)
485 {
486 JLOG(j_.warn()) << "Problem retrieving from " << sites_[siteIdx].activeResource->uri << " " << endpoint
487 << " " << ec.value() << ":" << ec.message();
488 onError("fetch error", true);
489 }
490 else
491 {
492 try
493 {
494 using namespace boost::beast::http;
495 switch (res.result())
496 {
497 case status::ok:
498 sites_[siteIdx].lastRequestSuccessful = true;
499 parseJsonResponse(res.body(), siteIdx, lock_sites);
500 break;
501 case status::moved_permanently:
502 case status::permanent_redirect:
503 case status::found:
504 case status::temporary_redirect: {
505 auto newLocation = processRedirect(res, siteIdx, lock_sites);
506 XRPL_ASSERT(
507 newLocation,
508 "xrpl::ValidatorSite::onSiteFetch : non-null "
509 "validator");
510 // for perm redirects, also update our starting URI
511 if (res.result() == status::moved_permanently || res.result() == status::permanent_redirect)
512 {
513 sites_[siteIdx].startingResource = newLocation;
514 }
515 makeRequest(newLocation, siteIdx, lock_sites);
516 return; // we are still fetching, so skip
517 // state update/notify below
518 }
519 default: {
520 JLOG(j_.warn()) << "Request for validator list at " << sites_[siteIdx].activeResource->uri
521 << " " << endpoint << " returned bad status: " << res.result_int();
522 onError("bad result code", true);
523 }
524 }
525 }
526 catch (std::exception const& ex)
527 {
528 JLOG(j_.error()) << "Exception in " << __func__ << ": " << ex.what();
529 onError(ex.what(), false);
530 }
531 }
532 sites_[siteIdx].activeResource.reset();
533 }
534
535 std::lock_guard lock_state{state_mutex_};
536 fetching_ = false;
537 if (!stopping_)
538 setTimer(lock_sites, lock_state);
539 cv_.notify_all();
540}
541
542void
543ValidatorSite::onTextFetch(boost::system::error_code const& ec, std::string const& res, std::size_t siteIdx)
544{
545 std::lock_guard lock_sites{sites_mutex_};
546 {
547 try
548 {
549 if (ec)
550 {
551 JLOG(j_.warn()) << "Problem retrieving from " << sites_[siteIdx].activeResource->uri << " "
552 << ec.value() << ": " << ec.message();
553 throw std::runtime_error{"fetch error"};
554 }
555
556 sites_[siteIdx].lastRequestSuccessful = true;
557
558 parseJsonResponse(res, siteIdx, lock_sites);
559 }
560 catch (std::exception const& ex)
561 {
562 JLOG(j_.error()) << "Exception in " << __func__ << ": " << ex.what();
563 sites_[siteIdx].lastRefreshStatus.emplace(
565 }
566 sites_[siteIdx].activeResource.reset();
567 }
568
569 std::lock_guard lock_state{state_mutex_};
570 fetching_ = false;
571 if (!stopping_)
572 setTimer(lock_sites, lock_state);
573 cv_.notify_all();
574}
575
578{
579 using namespace std::chrono;
580 using Int = Json::Value::Int;
581
583 Json::Value& jSites = (jrr[jss::validator_sites] = Json::arrayValue);
584 {
586 for (Site const& site : sites_)
587 {
590 uri << site.loadedResource->uri;
591 if (site.loadedResource != site.startingResource)
592 uri << " (redirects to " << site.startingResource->uri + ")";
593 v[jss::uri] = uri.str();
594 v[jss::next_refresh_time] = to_string(site.nextRefresh);
595 if (site.lastRefreshStatus)
596 {
597 v[jss::last_refresh_time] = to_string(site.lastRefreshStatus->refreshed);
598 v[jss::last_refresh_status] = to_string(site.lastRefreshStatus->disposition);
599 if (!site.lastRefreshStatus->message.empty())
600 v[jss::last_refresh_message] = site.lastRefreshStatus->message;
601 }
602 v[jss::refresh_interval_min] = static_cast<Int>(site.refreshInterval.count());
603 }
604 }
605 return jrr;
606}
607} // namespace xrpl
T clamp(T... args)
Unserialize a JSON document into a Value.
Definition json_reader.h:18
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:319
Stream debug() const
Definition Journal.h:301
Stream warn() const
Definition Journal.h:313
virtual HashRouter & getHashRouter()=0
virtual Config & config()=0
virtual boost::asio::io_context & getIOContext()=0
virtual Overlay & overlay()=0
virtual ValidatorList & validators()=0
virtual NetworkOPs & getOPs()=0
std::vector< std::string > loadLists()
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...
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...
void onRequestTimeout(std::size_t siteIdx, error_code const &ec)
request took too long
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.
bool load(std::vector< std::string > const &siteURIs)
Load configured site URIs.
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::atomic< bool > pending_
std::atomic< bool > fetching_
void start()
Start fetching lists from sites.
std::chrono::seconds const requestTimeout_
boost::asio::basic_waitable_timer< clock_type > timer_
boost::asio::ip::tcp::endpoint endpoint_type
void makeRequest(std::shared_ptr< Site::Resource > resource, std::size_t siteIdx, std::lock_guard< std::mutex > const &)
Initiate request to given resource.
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.
Json::Value getJson() const
Return JSON representation of configured validator sites.
std::condition_variable cv_
void parseJsonResponse(std::string const &res, std::size_t siteIdx, std::lock_guard< std::mutex > const &)
Parse json response from validator list site.
beast::Journal const j_
std::atomic< bool > stopping_
ValidatorSite(Application &app, std::optional< beast::Journal > j=std::nullopt, std::chrono::seconds timeout=std::chrono::seconds{20})
boost::system::error_code error_code
std::weak_ptr< detail::Work > work_
Application & app_
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 onTextFetch(boost::system::error_code const &ec, std::string const &res, std::size_t siteIdx)
Store latest list fetched from anywhere.
void onTimer(std::size_t siteIdx, error_code const &ec)
Fetch site whose time has come.
void stop()
Stop fetching lists from sites.
std::vector< Site > sites_
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
STL namespace.
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:11
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:6
sha512_half_hasher::result_type sha512Half(Args const &... args)
Returns the SHA512-Half of a series of objects.
Definition digest.h:205
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:598
@ 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)
unsigned short constexpr max_redirects
@ manifest
Manifest.
auto constexpr error_retry_interval
auto constexpr default_refresh_interval
T size(T... args)
T str(T... args)
std::shared_ptr< Resource > startingResource
the resource to request at <timer> intervals.
endpoint_type lastRequestEndpoint
clock_type::time_point nextRefresh
std::chrono::minutes refreshInterval
std::shared_ptr< Resource > loadedResource
the original uri as loaded from config
std::optional< std::uint16_t > port
T substr(T... args)
T to_string(T... args)
T what(T... args)