239 const std::string_view location
241 constexpr std::array allowed {
242 std::string_view {
"timeouts" },
243 std::string_view {
"retry" },
244 std::string_view {
"admission" },
245 std::string_view {
"cache" },
246 std::string_view {
"redirect_policy" },
247 std::string_view {
"maximum_artifact_bytes" },
249 reject_unknown_keys(object, allowed, location);
251 if (
const auto value = object.find(
"timeouts"); value != object.end()) {
252 constexpr std::array fields {
253 std::string_view {
"total_ms" },
254 std::string_view {
"connect_ms" },
255 std::string_view {
"read_ms" },
256 std::string_view {
"write_ms" },
257 std::string_view {
"pool_ms" },
260 *value, fields, std::string(location) +
".timeouts"
262 policy.timeouts.total = std::chrono::milliseconds(bounded_unsigned(
264 static_cast<std::uint64_t>(policy.timeouts.total.count()), 1U,
265 maximum_policy_milliseconds, std::string(location) +
".timeouts"
267 policy.timeouts.connect
268 = std::chrono::milliseconds(bounded_unsigned(
269 *value,
"connect_ms",
270 static_cast<std::uint64_t>(policy.timeouts.connect.count()),
271 1U, maximum_policy_milliseconds,
272 std::string(location) +
".timeouts"
274 policy.timeouts.read = std::chrono::milliseconds(bounded_unsigned(
276 static_cast<std::uint64_t>(policy.timeouts.read.count()), 1U,
277 maximum_policy_milliseconds, std::string(location) +
".timeouts"
279 policy.timeouts.write = std::chrono::milliseconds(bounded_unsigned(
281 static_cast<std::uint64_t>(policy.timeouts.write.count()), 1U,
282 maximum_policy_milliseconds, std::string(location) +
".timeouts"
284 policy.timeouts.pool = std::chrono::milliseconds(bounded_unsigned(
286 static_cast<std::uint64_t>(policy.timeouts.pool.count()), 1U,
287 maximum_policy_milliseconds, std::string(location) +
".timeouts"
290 if (
const auto value = object.find(
"retry"); value != object.end()) {
291 constexpr std::array fields {
292 std::string_view {
"maximum_attempts" },
293 std::string_view {
"initial_delay_ms" },
294 std::string_view {
"maximum_delay_ms" },
295 std::string_view {
"total_delay_budget_ms" },
296 std::string_view {
"respect_retry_after" },
299 *value, fields, std::string(location) +
".retry"
301 policy.retry.maximum_attempts
302 =
static_cast<std::size_t>(bounded_unsigned(
303 *value,
"maximum_attempts", policy.retry.maximum_attempts,
304 1U, 20U, std::string(location) +
".retry"
307 .initial_delay = std::chrono::milliseconds(bounded_unsigned(
308 *value,
"initial_delay_ms",
309 static_cast<std::uint64_t>(policy.retry.initial_delay.count()),
310 0U, maximum_policy_milliseconds,
311 std::string(location) +
".retry"
314 .maximum_delay = std::chrono::milliseconds(bounded_unsigned(
315 *value,
"maximum_delay_ms",
316 static_cast<std::uint64_t>(policy.retry.maximum_delay.count()),
317 0U, maximum_policy_milliseconds,
318 std::string(location) +
".retry"
320 policy.retry.total_delay_budget
321 = std::chrono::milliseconds(bounded_unsigned(
322 *value,
"total_delay_budget_ms",
323 static_cast<std::uint64_t>(
324 policy.retry.total_delay_budget.count()
326 0U, maximum_policy_milliseconds,
327 std::string(location) +
".retry"
329 policy.retry.respect_retry_after = optional_boolean(
330 *value,
"respect_retry_after", policy.retry.respect_retry_after,
331 std::string(location) +
".retry"
334 if (
const auto value = object.find(
"admission");
335 value != object.end()) {
336 constexpr std::array fields {
337 std::string_view {
"maximum_concurrency" },
338 std::string_view {
"minimum_interval_ms" },
341 *value, fields, std::string(location) +
".admission"
343 policy.admission.maximum_concurrency
344 =
static_cast<std::size_t>(bounded_unsigned(
345 *value,
"maximum_concurrency",
346 policy.admission.maximum_concurrency, 1U, 1'024U,
347 std::string(location) +
".admission"
349 policy.admission.minimum_interval
350 = std::chrono::milliseconds(bounded_unsigned(
351 *value,
"minimum_interval_ms",
352 static_cast<std::uint64_t>(
353 policy.admission.minimum_interval.count()
355 0U, maximum_policy_milliseconds,
356 std::string(location) +
".admission"
359 if (
const auto value = object.find(
"cache"); value != object.end()) {
360 constexpr std::array fields { std::string_view {
"ttl_seconds" } };
362 *value, fields, std::string(location) +
".cache"
364 policy.cache.ttl = std::chrono::seconds(bounded_unsigned(
365 *value,
"ttl_seconds",
366 static_cast<std::uint64_t>(policy.cache.ttl.count()), 0U,
367 maximum_cache_seconds, std::string(location) +
".cache"
370 if (
const auto value = object.find(
"redirect_policy");
371 value != object.end()) {
372 constexpr std::array fields {
373 std::string_view {
"follow" },
374 std::string_view {
"maximum_redirects" },
375 std::string_view {
"allow_https_to_http" },
376 std::string_view {
"allowed_hosts" },
379 *value, fields, std::string(location) +
".redirect_policy"
381 policy.redirects.follow = optional_boolean(
382 *value,
"follow", policy.redirects.follow,
383 std::string(location) +
".redirect_policy"
385 policy.redirects.maximum_redirects
386 =
static_cast<std::size_t>(bounded_unsigned(
387 *value,
"maximum_redirects",
388 policy.redirects.maximum_redirects, 0U, 20U,
389 std::string(location) +
".redirect_policy"
391 policy.redirects.allow_https_to_http = optional_boolean(
392 *value,
"allow_https_to_http",
393 policy.redirects.allow_https_to_http,
394 std::string(location) +
".redirect_policy"
396 if (
const auto hosts = value->find(
"allowed_hosts");
397 hosts != value->end()) {
398 if (!hosts->is_array() || hosts->empty()) {
399 throw std::invalid_argument(
400 std::string(location)
401 +
".redirect_policy.allowed_hosts must be a non-empty "
405 policy.redirects.allowed_hosts.clear();
406 for (
const auto& host : *hosts) {
407 if (!host.is_string()
408 || host.get_ref<
const std::string&>().empty()) {
409 throw std::invalid_argument(
410 std::string(location)
411 +
".redirect_policy.allowed_hosts contains an "
415 policy.redirects.allowed_hosts.push_back(
416 host.get<std::string>()
421 policy.maximum_artifact_bytes = bounded_unsigned(
422 object,
"maximum_artifact_bytes", policy.maximum_artifact_bytes, 1U,
423 static_cast<std::uint64_t>(
424 std::numeric_limits<std::int64_t>::max()
429 if (policy.timeouts.connect > policy.timeouts.total) {
430 throw std::invalid_argument(
431 std::string(location) +
" connect timeout exceeds total timeout"
434 if (policy.retry.initial_delay > policy.retry.maximum_delay) {
435 throw std::invalid_argument(
436 std::string(location)
437 +
" initial retry delay exceeds maximum retry delay"
440 if (!policy.redirects.follow
441 && policy.redirects.maximum_redirects != 0U) {
442 throw std::invalid_argument(
443 std::string(location)
444 +
" disables redirects but permits redirect attempts"
468 const std::string& value,
const std::string_view location
470 std::unique_ptr<CURLU,
decltype(&curl_url_cleanup)> url(
471 curl_url(), &curl_url_cleanup
474 || curl_url_set(url.get(), CURLUPART_URL, value.c_str(), 0)
476 throw std::invalid_argument(
477 std::string(location) +
" is not an absolute URL"
481 result
.scheme = ascii_lower(curl_url_part(url.get(), CURLUPART_SCHEME));
482 result
.host = ascii_lower(curl_url_part(url.get(), CURLUPART_HOST));
483 result
.path = curl_url_part(url.get(), CURLUPART_PATH);
484 result
.normalized = curl_url_part(url.get(), CURLUPART_URL);
485 const std::string port = curl_url_part(url.get(), CURLUPART_PORT);
487 throw std::invalid_argument(
488 std::string(location) +
" must use HTTP or HTTPS"
492 || !curl_url_part(url.get(), CURLUPART_USER).empty()
493 || !curl_url_part(url.get(), CURLUPART_PASSWORD).empty()
494 || !curl_url_part(url.get(), CURLUPART_FRAGMENT).empty()) {
495 throw std::invalid_argument(
496 std::string(location) +
" contains an unsafe URL authority"
503 const unsigned long converted = std::stoul(port);
504 if (converted == 0UL || converted > 65'535UL) {
505 throw std::out_of_range(
"port");
507 result
.port =
static_cast<
unsigned>(converted);
508 }
catch (
const std::exception&) {
509 throw std::invalid_argument(
510 std::string(location) +
" has an invalid port"
514 if (result
.path.empty()) {
733 std::vector<std::pair<std::string, std::string>> headers;
734 headers.reserve(request.headers.size());
735 for (
const auto& header : request.headers) {
736 const std::string name = ascii_lower(header.name);
737 const std::string value = sensitive_header(name)
738 ?
"sha256:" + crypto::sha256(header.value)
740 headers.emplace_back(name, value);
742 std::ranges::sort(headers);
743 nlohmann::ordered_json identity {
744 {
"door_id", request.door_id },
745 {
"endpoint_id", request.endpoint_id },
746 {
"operation", to_string(request.operation) },
747 {
"method", request.method == http_method::get ?
"GET" :
"POST" },
748 {
"url", request.url },
749 {
"headers", nlohmann::ordered_json::array() },
751 request.body_artifact ? request.body_artifact->sha256
752 : crypto::sha256(request.body) },
754 request.resume_artifact ? request.resume_artifact->sha256
757 request.expected_sha256.value_or(std::string {}) },
758 {
"maximum_bytes", request.max_bytes },
760 for (
const auto& [name, value] : headers) {
761 identity[
"headers"].push_back(
762 { {
"name", name }, {
"value", value } }
846 const fs::path& cache_root,
const std::string& cache_key,
849 const auto stored_unix
850 = std::chrono::duration_cast<std::chrono::seconds>(
851 std::chrono::system_clock::now().time_since_epoch()
854 const nlohmann::ordered_json document {
855 {
"cache_version", 1 },
856 {
"cache_key", cache_key },
857 {
"stored_unix", stored_unix },
863 const std::string bytes
864 = arachnespace::contracts::canonical_json(document) +
"\n";
867 const fs::path temporary = cache_root
868 / (
".stage-" + std::to_string(::getpid()) +
"-"
869 + std::to_string(sequence));
870 const fs::path target = cache_root / (cache_key +
".json");
871 const int descriptor = ::open(
873 O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600
875 if (descriptor < 0) {
878 std::size_t offset = 0;
880 while (offset < bytes.size()) {
881 const ssize_t count = ::write(
882 descriptor, bytes.data() + offset, bytes.size() - offset
884 if (count < 0 && errno == EINTR) {
891 offset +=
static_cast<std::size_t>(count);
893 if (written && ::fsync(descriptor) != 0) {
896 if (::close(descriptor) != 0) {
900 std::error_code ignored;
901 fs::remove(temporary, ignored);
904 std::error_code error;
905 fs::rename(temporary, target, error);
907 std::error_code ignored;
908 fs::remove(temporary, ignored);