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