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