rippled
Loading...
Searching...
No Matches
ValidatorSite.cpp
1//------------------------------------------------------------------------------
2/*
3 This file is part of rippled: https://github.com/ripple/rippled
4 Copyright (c) 2016 Ripple Labs Inc.
5
6 Permission to use, copy, modify, and/or distribute this software for any
7 purpose with or without fee is hereby granted, provided that the above
8 copyright notice and this permission notice appear in all copies.
9
10 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17*/
18//==============================================================================
19
20#include <xrpld/app/misc/ValidatorList.h>
21#include <xrpld/app/misc/ValidatorSite.h>
22#include <xrpld/app/misc/detail/WorkFile.h>
23#include <xrpld/app/misc/detail/WorkPlain.h>
24#include <xrpld/app/misc/detail/WorkSSL.h>
25
26#include <xrpl/json/json_reader.h>
27#include <xrpl/protocol/digest.h>
28#include <xrpl/protocol/jss.h>
29
30#include <algorithm>
31
32namespace ripple {
33
36unsigned short constexpr max_redirects = 3;
37
39{
40 if (!parseUrl(pUrl, uri))
41 throw std::runtime_error("URI '" + uri + "' cannot be parsed");
42
43 if (pUrl.scheme == "file")
44 {
45 if (!pUrl.domain.empty())
46 throw std::runtime_error("file URI cannot contain a hostname");
47
48#if BOOST_OS_WINDOWS
49 // Paths on Windows need the leading / removed
50 if (pUrl.path[0] == '/')
52#endif
53
54 if (pUrl.path.empty())
55 throw std::runtime_error("file URI must contain a path");
56 }
57 else if (pUrl.scheme == "http")
58 {
59 if (pUrl.domain.empty())
60 throw std::runtime_error("http URI must contain a hostname");
61
62 if (!pUrl.port)
63 pUrl.port = 80;
64 }
65 else if (pUrl.scheme == "https")
66 {
67 if (pUrl.domain.empty())
68 throw std::runtime_error("https URI must contain a hostname");
69
70 if (!pUrl.port)
71 pUrl.port = 443;
72 }
73 else
74 throw std::runtime_error("Unsupported scheme: '" + pUrl.scheme + "'");
75}
76
87
89 Application& app,
92 : app_{app}
93 , j_{j ? *j : app_.logs().journal("ValidatorSite")}
94 , timer_{app_.getIOContext()}
95 , fetching_{false}
96 , pending_{false}
97 , stopping_{false}
98 , requestTimeout_{timeout}
99{
100}
101
103{
105 if (timer_.expiry() > clock_type::time_point{})
106 {
107 if (!stopping_)
108 {
109 lock.unlock();
110 stop();
111 }
112 else
113 {
114 cv_.wait(lock, [&] { return !fetching_; });
115 }
116 }
117}
118
119bool
121{
122 auto const sites = app_.validators().loadLists();
123 return sites.empty() || load(sites, lock_sites);
124}
125
126bool
128{
129 JLOG(j_.debug()) << "Loading configured validator list sites";
130
132
133 return load(siteURIs, lock);
134}
135
136bool
138 std::vector<std::string> const& siteURIs,
139 std::lock_guard<std::mutex> const& lock_sites)
140{
141 // If no sites are provided, act as if a site failed to load.
142 if (siteURIs.empty())
143 {
144 return missingSite(lock_sites);
145 }
146
147 for (auto const& uri : siteURIs)
148 {
149 try
150 {
151 sites_.emplace_back(uri);
152 }
153 catch (std::exception const& e)
154 {
155 JLOG(j_.error())
156 << "Invalid validator site uri: " << uri << ": " << e.what();
157 return false;
158 }
159 }
160
161 JLOG(j_.debug()) << "Loaded " << siteURIs.size() << " sites";
162
163 return true;
164}
165
166void
168{
171 if (timer_.expiry() == clock_type::time_point{})
172 setTimer(l0, l1);
173}
174
175void
177{
179 cv_.wait(lock, [&] { return !pending_; });
180}
181
182void
184{
186 stopping_ = true;
187 // work::cancel() must be called before the
188 // cv wait in order to kick any asio async operations
189 // that might be pending.
190 if (auto sp = work_.lock())
191 sp->cancel();
192 cv_.wait(lock, [&] { return !fetching_; });
193
194 // docs indicate cancel() can throw, but this should be
195 // reconsidered if it changes to noexcept
196 try
197 {
198 timer_.cancel();
199 }
200 catch (boost::system::system_error const&)
201 {
202 }
203 stopping_ = false;
204 pending_ = false;
205 cv_.notify_all();
206}
207
208void
210 std::lock_guard<std::mutex> const& site_lock,
211 std::lock_guard<std::mutex> const& state_lock)
212{
213 auto next = std::min_element(
214 sites_.begin(), sites_.end(), [](Site const& a, Site const& b) {
215 return a.nextRefresh < b.nextRefresh;
216 });
217
218 if (next != sites_.end())
219 {
220 pending_ = next->nextRefresh <= clock_type::now();
221 cv_.notify_all();
222 timer_.expires_at(next->nextRefresh);
223 auto idx = std::distance(sites_.begin(), next);
224 timer_.async_wait([this, idx](boost::system::error_code const& ec) {
225 this->onTimer(idx, ec);
226 });
227 }
228}
229
230void
233 std::size_t siteIdx,
234 std::lock_guard<std::mutex> const& sites_lock)
235{
236 fetching_ = true;
237 sites_[siteIdx].activeResource = resource;
239 auto timeoutCancel = [this]() {
240 std::lock_guard lock_state{state_mutex_};
241 // docs indicate cancel_one() can throw, but this
242 // should be reconsidered if it changes to noexcept
243 try
244 {
245 timer_.cancel_one();
246 }
247 catch (boost::system::system_error const&)
248 {
249 }
250 };
251 auto onFetch = [this, siteIdx, timeoutCancel](
252 error_code const& err,
253 endpoint_type const& endpoint,
254 detail::response_type&& resp) {
255 timeoutCancel();
256 onSiteFetch(err, endpoint, std::move(resp), siteIdx);
257 };
258
259 auto onFetchFile = [this, siteIdx, timeoutCancel](
260 error_code const& err, std::string const& resp) {
261 timeoutCancel();
262 onTextFetch(err, resp, siteIdx);
263 };
264
265 JLOG(j_.debug()) << "Starting request for " << resource->uri;
266
267 if (resource->pUrl.scheme == "https")
268 {
269 // can throw...
271 resource->pUrl.domain,
272 resource->pUrl.path,
273 std::to_string(*resource->pUrl.port),
275 j_,
276 app_.config(),
277 sites_[siteIdx].lastRequestEndpoint,
278 sites_[siteIdx].lastRequestSuccessful,
279 onFetch);
280 }
281 else if (resource->pUrl.scheme == "http")
282 {
284 resource->pUrl.domain,
285 resource->pUrl.path,
286 std::to_string(*resource->pUrl.port),
288 sites_[siteIdx].lastRequestEndpoint,
289 sites_[siteIdx].lastRequestSuccessful,
290 onFetch);
291 }
292 else
293 {
294 BOOST_ASSERT(resource->pUrl.scheme == "file");
296 resource->pUrl.path, app_.getIOContext(), onFetchFile);
297 }
298
299 sites_[siteIdx].lastRequestSuccessful = false;
300 work_ = sp;
301 sp->run();
302 // start a timer for the request, which shouldn't take more
303 // than requestTimeout_ to complete
304 std::lock_guard lock_state{state_mutex_};
305 timer_.expires_after(requestTimeout_);
306 timer_.async_wait([this, siteIdx](boost::system::error_code const& ec) {
307 this->onRequestTimeout(siteIdx, ec);
308 });
309}
310
311void
313{
314 if (ec)
315 return;
316
317 {
318 std::lock_guard lock_site{sites_mutex_};
319 // In some circumstances, both this function and the response
320 // handler (onSiteFetch or onTextFetch) can get queued and
321 // processed. In all observed cases, the response handler
322 // processes a network error. Usually, this function runs first,
323 // but on extremely rare occasions, the response handler can run
324 // first, which will leave activeResource empty.
325 auto const& site = sites_[siteIdx];
326 if (site.activeResource)
327 JLOG(j_.warn()) << "Request for " << site.activeResource->uri
328 << " took too long";
329 else
330 JLOG(j_.error()) << "Request took too long, but a response has "
331 "already been processed";
332 }
333
334 std::lock_guard lock_state{state_mutex_};
335 if (auto sp = work_.lock())
336 sp->cancel();
337}
338
339void
341{
342 if (ec)
343 {
344 // Restart the timer if any errors are encountered, unless the error
345 // is from the wait operation being aborted due to a shutdown request.
346 if (ec != boost::asio::error::operation_aborted)
347 onSiteFetch(ec, {}, detail::response_type{}, siteIdx);
348 return;
349 }
350
351 try
352 {
354 sites_[siteIdx].nextRefresh =
355 clock_type::now() + sites_[siteIdx].refreshInterval;
356 sites_[siteIdx].redirCount = 0;
357 // the WorkSSL client ctor can throw if SSL init fails
358 makeRequest(sites_[siteIdx].startingResource, siteIdx, lock);
359 }
360 catch (std::exception const& ex)
361 {
362 JLOG(j_.error()) << "Exception in " << __func__ << ": " << ex.what();
364 boost::system::error_code{-1, boost::system::generic_category()},
365 {},
367 siteIdx);
368 }
369}
370
371void
373 std::string const& res,
374 std::size_t siteIdx,
375 std::lock_guard<std::mutex> const& sites_lock)
376{
377 Json::Value const body = [&res, siteIdx, this]() {
378 Json::Reader r;
379 Json::Value body;
380 if (!r.parse(res.data(), body))
381 {
382 JLOG(j_.warn()) << "Unable to parse JSON response from "
383 << sites_[siteIdx].activeResource->uri;
384 throw std::runtime_error{"bad json"};
385 }
386 return body;
387 }();
388
389 auto const [valid, version, blobs] = [&body]() {
390 // Check the easy fields first
391 bool valid = body.isObject() && body.isMember(jss::manifest) &&
392 body[jss::manifest].isString() && body.isMember(jss::version) &&
393 body[jss::version].isInt();
394 // Check the version-specific blob & signature fields
395 std::uint32_t version;
397 if (valid)
398 {
399 version = body[jss::version].asUInt();
400 blobs = ValidatorList::parseBlobs(version, body);
401 valid = !blobs.empty();
402 }
403 return std::make_tuple(valid, version, blobs);
404 }();
405
406 if (!valid)
407 {
408 JLOG(j_.warn()) << "Missing fields in JSON response from "
409 << sites_[siteIdx].activeResource->uri;
410 throw std::runtime_error{"missing fields"};
411 }
412
413 auto const manifest = body[jss::manifest].asString();
414 XRPL_ASSERT(
415 version == body[jss::version].asUInt(),
416 "ripple::ValidatorSite::parseJsonResponse : version match");
417 auto const& uri = sites_[siteIdx].activeResource->uri;
418 auto const hash = sha512Half(manifest, blobs, version);
419 auto const applyResult = app_.validators().applyListsAndBroadcast(
420 manifest,
421 version,
422 blobs,
423 uri,
424 hash,
425 app_.overlay(),
427 app_.getOPs());
428
429 sites_[siteIdx].lastRefreshStatus.emplace(
430 Site::Status{clock_type::now(), applyResult.bestDisposition(), ""});
431
432 for (auto const& [disp, count] : applyResult.dispositions)
433 {
434 switch (disp)
435 {
437 JLOG(j_.debug()) << "Applied " << count
438 << " new validator list(s) from " << uri;
439 break;
441 JLOG(j_.debug()) << "Applied " << count
442 << " expired validator list(s) from " << uri;
443 break;
445 JLOG(j_.debug())
446 << "Ignored " << count
447 << " validator list(s) with current sequence from " << uri;
448 break;
450 JLOG(j_.debug()) << "Processed " << count
451 << " future validator list(s) from " << uri;
452 break;
454 JLOG(j_.debug())
455 << "Ignored " << count
456 << " validator list(s) with future known sequence from "
457 << uri;
458 break;
460 JLOG(j_.warn()) << "Ignored " << count
461 << "stale validator list(s) from " << uri;
462 break;
464 JLOG(j_.warn()) << "Ignored " << count
465 << " untrusted validator list(s) from " << uri;
466 break;
468 JLOG(j_.warn()) << "Ignored " << count
469 << " invalid validator list(s) from " << uri;
470 break;
472 JLOG(j_.warn())
473 << "Ignored " << count
474 << " unsupported version validator list(s) from " << uri;
475 break;
476 default:
477 BOOST_ASSERT(false);
478 }
479 }
480
481 if (body.isMember(jss::refresh_interval) &&
482 body[jss::refresh_interval].isNumeric())
483 {
484 using namespace std::chrono_literals;
485 std::chrono::minutes const refresh = std::clamp(
486 std::chrono::minutes{body[jss::refresh_interval].asUInt()},
487 1min,
489 sites_[siteIdx].refreshInterval = refresh;
490 sites_[siteIdx].nextRefresh =
491 clock_type::now() + sites_[siteIdx].refreshInterval;
492 }
493}
494
498 std::size_t siteIdx,
499 std::lock_guard<std::mutex> const& sites_lock)
500{
501 using namespace boost::beast::http;
503 if (res.find(field::location) == res.end() || res[field::location].empty())
504 {
505 JLOG(j_.warn()) << "Request for validator list at "
506 << sites_[siteIdx].activeResource->uri
507 << " returned a redirect with no Location.";
508 throw std::runtime_error{"missing location"};
509 }
510
511 if (sites_[siteIdx].redirCount == max_redirects)
512 {
513 JLOG(j_.warn()) << "Exceeded max redirects for validator list at "
514 << sites_[siteIdx].loadedResource->uri;
515 throw std::runtime_error{"max redirects"};
516 }
517
518 JLOG(j_.debug()) << "Got redirect for validator list from "
519 << sites_[siteIdx].activeResource->uri
520 << " to new location " << res[field::location];
521
522 try
523 {
524 newLocation =
525 std::make_shared<Site::Resource>(std::string(res[field::location]));
526 ++sites_[siteIdx].redirCount;
527 if (newLocation->pUrl.scheme != "http" &&
528 newLocation->pUrl.scheme != "https")
529 throw std::runtime_error(
530 "invalid scheme in redirect " + newLocation->pUrl.scheme);
531 }
532 catch (std::exception const& ex)
533 {
534 JLOG(j_.error()) << "Invalid redirect location: "
535 << res[field::location];
536 throw;
537 }
538 return newLocation;
539}
540
541void
543 boost::system::error_code const& ec,
544 endpoint_type const& endpoint,
546 std::size_t siteIdx)
547{
548 std::lock_guard lock_sites{sites_mutex_};
549 {
550 if (endpoint != endpoint_type{})
551 sites_[siteIdx].lastRequestEndpoint = endpoint;
552 JLOG(j_.debug()) << "Got completion for "
553 << sites_[siteIdx].activeResource->uri << " "
554 << endpoint;
555 auto onError = [&](std::string const& errMsg, bool retry) {
556 sites_[siteIdx].lastRefreshStatus.emplace(Site::Status{
558 if (retry)
559 sites_[siteIdx].nextRefresh =
561
562 // See if there's a copy saved locally from last time we
563 // saw the list.
564 missingSite(lock_sites);
565 };
566 if (ec)
567 {
568 JLOG(j_.warn())
569 << "Problem retrieving from "
570 << sites_[siteIdx].activeResource->uri << " " << endpoint << " "
571 << ec.value() << ":" << ec.message();
572 onError("fetch error", true);
573 }
574 else
575 {
576 try
577 {
578 using namespace boost::beast::http;
579 switch (res.result())
580 {
581 case status::ok:
582 sites_[siteIdx].lastRequestSuccessful = true;
583 parseJsonResponse(res.body(), siteIdx, lock_sites);
584 break;
585 case status::moved_permanently:
586 case status::permanent_redirect:
587 case status::found:
588 case status::temporary_redirect: {
589 auto newLocation =
590 processRedirect(res, siteIdx, lock_sites);
591 XRPL_ASSERT(
592 newLocation,
593 "ripple::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, lock_sites);
602 return; // we are still fetching, so skip
603 // state update/notify below
604 }
605 default: {
606 JLOG(j_.warn())
607 << "Request for validator list at "
608 << sites_[siteIdx].activeResource->uri << " "
609 << endpoint
610 << " returned bad status: " << res.result_int();
611 onError("bad result code", true);
612 }
613 }
614 }
615 catch (std::exception const& ex)
616 {
617 JLOG(j_.error())
618 << "Exception in " << __func__ << ": " << ex.what();
619 onError(ex.what(), false);
620 }
621 }
622 sites_[siteIdx].activeResource.reset();
623 }
624
625 std::lock_guard lock_state{state_mutex_};
626 fetching_ = false;
627 if (!stopping_)
628 setTimer(lock_sites, lock_state);
629 cv_.notify_all();
630}
631
632void
634 boost::system::error_code const& ec,
635 std::string const& res,
636 std::size_t siteIdx)
637{
638 std::lock_guard lock_sites{sites_mutex_};
639 {
640 try
641 {
642 if (ec)
643 {
644 JLOG(j_.warn()) << "Problem retrieving from "
645 << sites_[siteIdx].activeResource->uri << " "
646 << ec.value() << ": " << ec.message();
647 throw std::runtime_error{"fetch error"};
648 }
649
650 sites_[siteIdx].lastRequestSuccessful = true;
651
652 parseJsonResponse(res, siteIdx, lock_sites);
653 }
654 catch (std::exception const& ex)
655 {
656 JLOG(j_.error())
657 << "Exception in " << __func__ << ": " << ex.what();
658 sites_[siteIdx].lastRefreshStatus.emplace(Site::Status{
660 }
661 sites_[siteIdx].activeResource.reset();
662 }
663
664 std::lock_guard lock_state{state_mutex_};
665 fetching_ = false;
666 if (!stopping_)
667 setTimer(lock_sites, lock_state);
668 cv_.notify_all();
669}
670
673{
674 using namespace std::chrono;
675 using Int = Json::Value::Int;
676
678 Json::Value& jSites = (jrr[jss::validator_sites] = Json::arrayValue);
679 {
681 for (Site const& site : sites_)
682 {
685 uri << site.loadedResource->uri;
686 if (site.loadedResource != site.startingResource)
687 uri << " (redirects to " << site.startingResource->uri + ")";
688 v[jss::uri] = uri.str();
689 v[jss::next_refresh_time] = to_string(site.nextRefresh);
690 if (site.lastRefreshStatus)
691 {
692 v[jss::last_refresh_time] =
693 to_string(site.lastRefreshStatus->refreshed);
694 v[jss::last_refresh_status] =
695 to_string(site.lastRefreshStatus->disposition);
696 if (!site.lastRefreshStatus->message.empty())
697 v[jss::last_refresh_message] =
698 site.lastRefreshStatus->message;
699 }
700 v[jss::refresh_interval_min] =
701 static_cast<Int>(site.refreshInterval.count());
702 }
703 }
704 return jrr;
705}
706} // namespace ripple
T clamp(T... args)
Unserialize a JSON document into a Value.
Definition json_reader.h:39
bool parse(std::string const &document, Value &root)
Read a Value from a JSON document.
Represents a JSON value.
Definition json_value.h:149
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:157
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:346
Stream debug() const
Definition Journal.h:328
Stream warn() const
Definition Journal.h:340
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:44
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:45
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:31
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:25
@ 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:630
@ 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:224
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.
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)