Arachne 1.0
Arachne - the perpetual stitcher of Wikidata entities.
Loading...
Searching...
No Matches
arachne::pheidippides::anonymous_namespace{hardened_transport.cpp} Namespace Reference

Classes

struct  timeout_settings
struct  retry_settings
struct  admission_settings
struct  cache_settings
struct  configured_redirect_policy
struct  policy_settings
struct  parsed_url
struct  authentication_settings
class  admission_gate
class  admission_lease
struct  endpoint_configuration
struct  door_configuration
struct  cached_artifact
struct  flight_state

Typedefs

using json = nlohmann::json

Enumerations

enum class  authentication_mode {
  none , bearer_environment , header_environment , none ,
  bearer_environment , header_environment
}
enum class  authentication_mode {
  none , bearer_environment , header_environment , none ,
  bearer_environment , header_environment
}

Functions

std::string ascii_lower (std::string value)
bool safe_token (const std::string_view value)
bool safe_environment_name (const std::string_view value)
bool safe_http_header_name (const std::string_view value)
bool safe_header_value (const std::string_view value, const std::size_t maximum_bytes)
void reject_unknown_keys (const json &object, const std::span< const std::string_view > allowed, const std::string_view location)
const jsonrequired_object (const json &object, const std::string_view key, const std::string_view location)
std::string required_string (const json &object, const std::string_view key, const std::string_view location)
std::uint64_t bounded_unsigned (const json &object, const std::string_view key, const std::uint64_t fallback, const std::uint64_t minimum, const std::uint64_t maximum, const std::string_view location)
bool optional_boolean (const json &object, const std::string_view key, const bool fallback, const std::string_view location)
void apply_settings (policy_settings &policy, const json &object, const std::string_view location)
std::string curl_url_part (CURLU *url, const CURLUPart part)
parsed_url parse_absolute_url (const std::string &value, const std::string_view location)
bool within_base_url (const parsed_url &request, const parsed_url &base)
std::string configured_authority (const parsed_url &url)
authentication_settings parse_authentication (const json &object, const std::string_view location)
acquired_artifact_v1 policy_failure (const fetch_request_v1 &request, const transport_status status, std::string message)
bool sensitive_header (const std::string_view name)
std::string request_cache_key (const fetch_request_v1 &request)
bool safe_cached_file (const fs::path &root, const std::string &storage_ref, const std::string &expected_sha256, const std::uint64_t expected_size)
std::optional< cached_artifactread_cache_entry (const fs::path &root, const fs::path &cache_root, const std::string &cache_key)
void write_cache_entry (const fs::path &cache_root, const std::string &cache_key, const acquired_artifact_v1 &acquired)
acquired_artifact_v1 cached_receipt (const fetch_request_v1 &request, const cached_artifact &cached, const delivery_mode mode)

Variables

constexpr std::uint64_t maximum_policy_milliseconds = 86'400'000U
constexpr std::uint64_t maximum_cache_seconds = 31'536'000U
constexpr std::uint64_t maximum_cache_document_bytes = 64U * 1024U
std::atomic< std::uint64_t > cache_stage_sequence { 0 }

Typedef Documentation

◆ json

using arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::json = nlohmann::json

Definition at line 36 of file hardened_transport.cpp.

Enumeration Type Documentation

◆ authentication_mode [1/2]

enum class arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::authentication_mode
strong

◆ authentication_mode [2/2]

enum class arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::authentication_mode
strong
Enumerator
none 
bearer_environment 
header_environment 
none 
bearer_environment 
header_environment 

Definition at line 537 of file hardened_transport.cpp.

537 {
538 none,
541 };

Function Documentation

◆ apply_settings()

void arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::apply_settings ( policy_settings & policy,
const json & object,
const std::string_view location )

Definition at line 237 of file hardened_transport.cpp.

240 {
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" },
248 };
249 reject_unknown_keys(object, allowed, location);
250
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" },
258 };
260 *value, fields, std::string(location) + ".timeouts"
261 );
262 policy.timeouts.total = std::chrono::milliseconds(bounded_unsigned(
263 *value, "total_ms",
264 static_cast<std::uint64_t>(policy.timeouts.total.count()), 1U,
265 maximum_policy_milliseconds, std::string(location) + ".timeouts"
266 ));
267 policy.timeouts.connect
268 = std::chrono::milliseconds(bounded_unsigned(
269 *value, "connect_ms",
270 static_cast<std::uint64_t>(policy.timeouts.connect.count()),
272 std::string(location) + ".timeouts"
273 ));
274 policy.timeouts.read = std::chrono::milliseconds(bounded_unsigned(
275 *value, "read_ms",
276 static_cast<std::uint64_t>(policy.timeouts.read.count()), 1U,
277 maximum_policy_milliseconds, std::string(location) + ".timeouts"
278 ));
279 policy.timeouts.write = std::chrono::milliseconds(bounded_unsigned(
280 *value, "write_ms",
281 static_cast<std::uint64_t>(policy.timeouts.write.count()), 1U,
282 maximum_policy_milliseconds, std::string(location) + ".timeouts"
283 ));
284 policy.timeouts.pool = std::chrono::milliseconds(bounded_unsigned(
285 *value, "pool_ms",
286 static_cast<std::uint64_t>(policy.timeouts.pool.count()), 1U,
287 maximum_policy_milliseconds, std::string(location) + ".timeouts"
288 ));
289 }
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" },
297 };
299 *value, fields, std::string(location) + ".retry"
300 );
302 = static_cast<std::size_t>(bounded_unsigned(
303 *value, "maximum_attempts", policy.retry.maximum_attempts,
304 1U, 20U, std::string(location) + ".retry"
305 ));
306 policy.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()),
311 std::string(location) + ".retry"
312 ));
313 policy.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()),
318 std::string(location) + ".retry"
319 ));
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()
325 ),
327 std::string(location) + ".retry"
328 ));
330 *value, "respect_retry_after", policy.retry.respect_retry_after,
331 std::string(location) + ".retry"
332 );
333 }
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" },
339 };
341 *value, fields, std::string(location) + ".admission"
342 );
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"
348 ));
350 = std::chrono::milliseconds(bounded_unsigned(
351 *value, "minimum_interval_ms",
352 static_cast<std::uint64_t>(
353 policy.admission.minimum_interval.count()
354 ),
356 std::string(location) + ".admission"
357 ));
358 }
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"
363 );
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"
368 ));
369 }
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" },
377 };
379 *value, fields, std::string(location) + ".redirect_policy"
380 );
382 *value, "follow", policy.redirects.follow,
383 std::string(location) + ".redirect_policy"
384 );
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"
390 ));
392 *value, "allow_https_to_http",
394 std::string(location) + ".redirect_policy"
395 );
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 "
402 "array"
403 );
404 }
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 "
412 "invalid host"
413 );
414 }
415 policy.redirects.allowed_hosts.push_back(
416 host.get<std::string>()
417 );
418 }
419 }
420 }
422 object, "maximum_artifact_bytes", policy.maximum_artifact_bytes, 1U,
423 static_cast<std::uint64_t>(
424 std::numeric_limits<std::int64_t>::max()
425 ),
426 location
427 );
428
429 if (policy.timeouts.connect > policy.timeouts.total) {
430 throw std::invalid_argument(
431 std::string(location) + " connect timeout exceeds total timeout"
432 );
433 }
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"
438 );
439 }
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"
445 );
446 }
447 }
void reject_unknown_keys(const json &object, const std::span< const std::string_view > allowed, const std::string_view location)
std::uint64_t bounded_unsigned(const json &object, const std::string_view key, const std::uint64_t fallback, const std::uint64_t minimum, const std::uint64_t maximum, const std::string_view location)
bool optional_boolean(const json &object, const std::string_view key, const bool fallback, const std::string_view location)

◆ ascii_lower()

std::string arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::ascii_lower ( std::string value)
nodiscard

Definition at line 44 of file hardened_transport.cpp.

44 {
45 std::ranges::transform(value, value.begin(), [](const char character) {
46 return static_cast<char>(
47 std::tolower(static_cast<unsigned char>(character))
48 );
49 });
50 return value;
51 }

Referenced by arachne::pheidippides::hardened_transport::implementation::implementation(), parse_authentication(), and sensitive_header().

Here is the caller graph for this function:

◆ bounded_unsigned()

std::uint64_t arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::bounded_unsigned ( const json & object,
const std::string_view key,
const std::uint64_t fallback,
const std::uint64_t minimum,
const std::uint64_t maximum,
const std::string_view location )
nodiscard

Definition at line 146 of file hardened_transport.cpp.

150 {
151 const auto value = object.find(key);
152 if (value == object.end()) {
153 return fallback;
154 }
155 if (!value->is_number_unsigned() && !value->is_number_integer()) {
156 throw std::invalid_argument(
157 std::string(location) + "." + std::string(key)
158 + " must be an integer"
159 );
160 }
161 std::uint64_t result = 0;
162 try {
163 result = value->get<std::uint64_t>();
164 } catch (const json::exception&) {
165 throw std::invalid_argument(
166 std::string(location) + "." + std::string(key)
167 + " must be non-negative"
168 );
169 }
170 if (result < minimum || result > maximum) {
171 throw std::invalid_argument(
172 std::string(location) + "." + std::string(key)
173 + " is outside the supported range"
174 );
175 }
176 return result;
177 }

◆ cached_receipt()

acquired_artifact_v1 arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::cached_receipt ( const fetch_request_v1 & request,
const cached_artifact & cached,
const delivery_mode mode )
nodiscard

Definition at line 912 of file hardened_transport.cpp.

915 {
916 const auto now = std::chrono::system_clock::now();
918 result.artifact_id = "artifact-" + request.request_id;
919 result.request_id = request.request_id;
920 result.door_id = request.door_id;
921 result.operation = request.operation;
922 result.delivered_via = mode;
924 result.source_url = request.url;
925 result.effective_url = request.url;
926 result.http_status = cached.http_status;
927 result.artifact_ref = cached.artifact_ref;
928 result.sha256 = cached.sha256;
929 result.byte_count = cached.byte_count;
930 result.started_at = now;
931 result.completed_at = now;
932 return result;
933 }
std::chrono::system_clock::time_point completed_at
std::chrono::system_clock::time_point started_at

References arachne::pheidippides::acquired_artifact_v1::artifact_id, arachne::pheidippides::acquired_artifact_v1::artifact_ref, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::cached_artifact::artifact_ref, arachne::pheidippides::acquired_artifact_v1::byte_count, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::cached_artifact::byte_count, arachne::pheidippides::delivered, arachne::pheidippides::acquired_artifact_v1::delivered_via, arachne::pheidippides::acquired_artifact_v1::door_id, arachne::pheidippides::fetch_request_v1::door_id, arachne::pheidippides::acquired_artifact_v1::effective_url, arachne::pheidippides::acquired_artifact_v1::http_status, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::cached_artifact::http_status, arachne::pheidippides::acquired_artifact_v1::operation, arachne::pheidippides::fetch_request_v1::operation, arachne::pheidippides::acquired_artifact_v1::request_id, arachne::pheidippides::fetch_request_v1::request_id, arachne::pheidippides::acquired_artifact_v1::sha256, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::cached_artifact::sha256, arachne::pheidippides::acquired_artifact_v1::source_url, arachne::pheidippides::acquired_artifact_v1::status, and arachne::pheidippides::fetch_request_v1::url.

◆ configured_authority()

std::string arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::configured_authority ( const parsed_url & url)
nodiscard

◆ curl_url_part()

std::string arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::curl_url_part ( CURLU * url,
const CURLUPart part )
nodiscard

Definition at line 457 of file hardened_transport.cpp.

457 {
458 char* raw = nullptr;
459 const CURLUcode status = curl_url_get(url, part, &raw, 0);
460 if (status != CURLUE_OK || raw == nullptr) {
461 return {};
462 }
463 std::unique_ptr<char, decltype(&curl_free)> owned(raw, &curl_free);
464 return std::string(raw);
465 }

◆ optional_boolean()

bool arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::optional_boolean ( const json & object,
const std::string_view key,
const bool fallback,
const std::string_view location )
nodiscard

Definition at line 179 of file hardened_transport.cpp.

182 {
183 const auto value = object.find(key);
184 if (value == object.end()) {
185 return fallback;
186 }
187 if (!value->is_boolean()) {
188 throw std::invalid_argument(
189 std::string(location) + "." + std::string(key)
190 + " must be boolean"
191 );
192 }
193 return value->get<bool>();
194 }

◆ parse_absolute_url()

parsed_url arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parse_absolute_url ( const std::string & value,
const std::string_view location )
nodiscard

Definition at line 467 of file hardened_transport.cpp.

469 {
470 std::unique_ptr<CURLU, decltype(&curl_url_cleanup)> url(
471 curl_url(), &curl_url_cleanup
472 );
473 if (!url
474 || curl_url_set(url.get(), CURLUPART_URL, value.c_str(), 0)
475 != CURLUE_OK) {
476 throw std::invalid_argument(
477 std::string(location) + " is not an absolute URL"
478 );
479 }
480 parsed_url result;
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);
486 if (result.scheme != "http" && result.scheme != "https") {
487 throw std::invalid_argument(
488 std::string(location) + " must use HTTP or HTTPS"
489 );
490 }
491 if (result.host.empty() || result.normalized.empty()
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"
497 );
498 }
499 if (port.empty()) {
500 result.port = result.scheme == "https" ? 443U : 80U;
501 } else {
502 try {
503 const unsigned long converted = std::stoul(port);
504 if (converted == 0UL || converted > 65'535UL) {
505 throw std::out_of_range("port");
506 }
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"
511 );
512 }
513 }
514 if (result.path.empty()) {
515 result.path = "/";
516 }
517 return result;
518 }

References arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parsed_url::host, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parsed_url::normalized, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parsed_url::path, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parsed_url::port, and arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parsed_url::scheme.

◆ parse_authentication()

authentication_settings arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parse_authentication ( const json & object,
const std::string_view location )
nodiscard

Definition at line 649 of file hardened_transport.cpp.

649 {
650 constexpr std::array fields {
651 std::string_view { "mode" },
652 std::string_view { "secret_name" },
653 std::string_view { "header_name" },
654 };
655 reject_unknown_keys(object, fields, location);
657 const std::string mode = required_string(object, "mode", location);
658 if (mode == "none") {
659 if (object.size() != 1U) {
660 throw std::invalid_argument(
661 std::string(location)
662 + " mode none cannot declare secret fields"
663 );
664 }
665 return result;
666 }
667 result.secret_name = required_string(object, "secret_name", location);
668 if (!safe_environment_name(result.secret_name)) {
669 throw std::invalid_argument(
670 std::string(location)
671 + ".secret_name is not an environment name"
672 );
673 }
674 if (mode == "bearer_env") {
676 result.header_name = "Authorization";
677 if (object.contains("header_name")) {
678 throw std::invalid_argument(
679 std::string(location)
680 + " bearer_env cannot override its header"
681 );
682 }
683 return result;
684 }
685 if (mode == "header_env") {
687 result.header_name
688 = required_string(object, "header_name", location);
689 const std::string lowered = ascii_lower(result.header_name);
690 if (lowered == "host"
691 || !safe_http_header_name(result.header_name)) {
692 throw std::invalid_argument(
693 std::string(location) + ".header_name is unsafe"
694 );
695 }
696 return result;
697 }
698 throw std::invalid_argument(
699 std::string(location) + ".mode is unsupported"
700 );
701 }
std::string required_string(const json &object, const std::string_view key, const std::string_view location)

References ascii_lower(), bearer_environment, header_environment, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::authentication_settings::header_name, and arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::authentication_settings::mode.

Here is the call graph for this function:

◆ policy_failure()

acquired_artifact_v1 arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::policy_failure ( const fetch_request_v1 & request,
const transport_status status,
std::string message )
nodiscard

◆ read_cache_entry()

std::optional< cached_artifact > arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::read_cache_entry ( const fs::path & root,
const fs::path & cache_root,
const std::string & cache_key )
nodiscard

Definition at line 806 of file hardened_transport.cpp.

809 {
810 const fs::path path = cache_root / (cache_key + ".json");
811 std::error_code error;
812 const auto state = fs::symlink_status(path, error);
813 if (error || fs::is_symlink(state) || !fs::is_regular_file(state)) {
814 return std::nullopt;
815 }
816 const auto size = fs::file_size(path, error);
817 if (error || size > maximum_cache_document_bytes) {
818 return std::nullopt;
819 }
820 try {
821 std::ifstream input(path, std::ios::binary);
822 const json document = json::parse(input);
823 if (!document.is_object() || document.value("cache_version", 0) != 1
824 || document.value("cache_key", std::string {}) != cache_key) {
825 return std::nullopt;
826 }
827 cached_artifact result {
828 .artifact_ref = document.at("artifact_ref").get<std::string>(),
829 .sha256 = document.at("sha256").get<std::string>(),
830 .byte_count = document.at("byte_count").get<std::uint64_t>(),
831 .http_status = document.at("http_status").get<long>(),
832 .stored_unix = document.at("stored_unix").get<std::int64_t>(),
833 };
834 if (!safe_cached_file(
835 root, result.artifact_ref, result.sha256, result.byte_count
836 )) {
837 return std::nullopt;
838 }
839 return result;
840 } catch (const std::exception&) {
841 return std::nullopt;
842 }
843 }
bool safe_cached_file(const fs::path &root, const std::string &storage_ref, const std::string &expected_sha256, const std::uint64_t expected_size)

References arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::cached_artifact::artifact_ref, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::cached_artifact::byte_count, maximum_cache_document_bytes, safe_cached_file(), and arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::cached_artifact::sha256.

Here is the call graph for this function:

◆ reject_unknown_keys()

void arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::reject_unknown_keys ( const json & object,
const std::span< const std::string_view > allowed,
const std::string_view location )

Definition at line 98 of file hardened_transport.cpp.

101 {
102 if (!object.is_object()) {
103 throw std::invalid_argument(
104 std::string(location) + " must be an object"
105 );
106 }
107 for (const auto& [key, unused] : object.items()) {
108 static_cast<void>(unused);
109 if (std::ranges::find(allowed, key) == allowed.end()) {
110 throw std::invalid_argument(
111 std::string(location) + " contains unknown field " + key
112 );
113 }
114 }
115 }

◆ request_cache_key()

std::string arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::request_cache_key ( const fetch_request_v1 & request)
nodiscard

Definition at line 732 of file hardened_transport.cpp.

732 {
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)
739 : header.value;
740 headers.emplace_back(name, value);
741 }
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() },
750 { "body_sha256",
751 request.body_artifact ? request.body_artifact->sha256
752 : crypto::sha256(request.body) },
753 { "resume_sha256",
754 request.resume_artifact ? request.resume_artifact->sha256
755 : std::string {} },
756 { "expected_sha256",
757 request.expected_sha256.value_or(std::string {}) },
758 { "maximum_bytes", request.max_bytes },
759 };
760 for (const auto& [name, value] : headers) {
761 identity["headers"].push_back(
762 { { "name", name }, { "value", value } }
763 );
764 }
765 return crypto::sha256(
767 );
768 }
std::string sha256(std::span< const std::byte > bytes)
Definition crypto.cpp:209
std::string_view to_string(transport_operation value) noexcept
std::string canonical_json(const nlohmann::json &document)
std::optional< body_artifact_reference > resume_artifact
Definition transport.hpp:81
std::optional< std::string > expected_sha256
Definition transport.hpp:93
std::vector< http_header > headers
Definition transport.hpp:78
std::optional< body_artifact_reference > body_artifact
Definition transport.hpp:80

References arachne::crypto::sha256().

Referenced by arachne::pheidippides::hardened_transport::implementation::execute().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ required_object()

const json & arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::required_object ( const json & object,
const std::string_view key,
const std::string_view location )
nodiscard

Definition at line 117 of file hardened_transport.cpp.

120 {
121 const auto value = object.find(key);
122 if (value == object.end() || !value->is_object()) {
123 throw std::invalid_argument(
124 std::string(location) + "." + std::string(key)
125 + " must be an object"
126 );
127 }
128 return *value;
129 }

◆ required_string()

std::string arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::required_string ( const json & object,
const std::string_view key,
const std::string_view location )
nodiscard

Definition at line 131 of file hardened_transport.cpp.

134 {
135 const auto value = object.find(key);
136 if (value == object.end() || !value->is_string()
137 || value->get_ref<const std::string&>().empty()) {
138 throw std::invalid_argument(
139 std::string(location) + "." + std::string(key)
140 + " must be a non-empty string"
141 );
142 }
143 return value->get<std::string>();
144 }

◆ safe_cached_file()

bool arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::safe_cached_file ( const fs::path & root,
const std::string & storage_ref,
const std::string & expected_sha256,
const std::uint64_t expected_size )
nodiscard

Definition at line 778 of file hardened_transport.cpp.

781 {
782 if (!crypto::is_safe_relative_artifact_ref(storage_ref)) {
783 return false;
784 }
785 fs::path current = root;
786 for (const auto& component : fs::path(storage_ref)) {
787 current /= component;
788 std::error_code error;
789 const auto state = fs::symlink_status(current, error);
790 if (error || fs::is_symlink(state)) {
791 return false;
792 }
793 }
794 std::error_code error;
795 if (!fs::is_regular_file(current)
796 || fs::file_size(current, error) != expected_size || error) {
797 return false;
798 }
799 try {
800 return crypto::sha256_file(current) == expected_sha256;
801 } catch (const std::exception&) {
802 return false;
803 }
804 }
bool is_safe_relative_artifact_ref(std::string_view artifact_ref) noexcept
Definition crypto.cpp:249
std::string sha256_file(const std::filesystem::path &path)
Definition crypto.cpp:221

References arachne::crypto::sha256_file().

Referenced by read_cache_entry().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ safe_environment_name()

bool arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::safe_environment_name ( const std::string_view value)
nodiscard

Definition at line 64 of file hardened_transport.cpp.

64 {
65 if (value.empty()
66 || (std::isalpha(static_cast<unsigned char>(value.front())) == 0
67 && value.front() != '_')) {
68 return false;
69 }
70 return std::ranges::all_of(value.substr(1), [](const char character) {
71 return std::isalnum(static_cast<unsigned char>(character)) != 0
72 || character == '_';
73 });
74 }

◆ safe_header_value()

bool arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::safe_header_value ( const std::string_view value,
const std::size_t maximum_bytes )
nodiscard

Definition at line 88 of file hardened_transport.cpp.

90 {
91 return !value.empty() && value.size() <= maximum_bytes
92 && std::ranges::none_of(value, [](const char character) {
93 return character == '\0' || character == '\r'
94 || character == '\n';
95 });
96 }

◆ safe_http_header_name()

bool arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::safe_http_header_name ( const std::string_view value)
nodiscard

Definition at line 76 of file hardened_transport.cpp.

76 {
77 if (value.empty()) {
78 return false;
79 }
80 constexpr std::string_view separators = "()<>@,;:\\\"/[]?={} \t";
81 return std::ranges::none_of(value, [&](const char character) {
82 const auto byte = static_cast<unsigned char>(character);
83 return byte < 0x20U || byte == 0x7fU
84 || separators.find(character) != std::string_view::npos;
85 });
86 }

◆ safe_token()

bool arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::safe_token ( const std::string_view value)
nodiscard

Definition at line 53 of file hardened_transport.cpp.

53 {
54 if (value.empty()
55 || std::isalnum(static_cast<unsigned char>(value.front())) == 0) {
56 return false;
57 }
58 return std::ranges::all_of(value.substr(1), [](const char character) {
59 return std::isalnum(static_cast<unsigned char>(character)) != 0
60 || character == '.' || character == '_' || character == '-';
61 });
62 }

◆ sensitive_header()

bool arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::sensitive_header ( const std::string_view name)
nodiscard

Definition at line 722 of file hardened_transport.cpp.

722 {
723 const std::string lowered = ascii_lower(std::string(name));
724 return lowered == "authorization" || lowered == "proxy-authorization"
725 || lowered == "cookie" || lowered == "set-cookie"
726 || lowered == "x-api-key" || lowered == "api-key"
727 || lowered == "x-goog-api-key" || lowered == "x-auth-token"
728 || lowered == "x-access-token" || lowered == "private-token";
729 }

References ascii_lower().

Here is the call graph for this function:

◆ within_base_url()

bool arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::within_base_url ( const parsed_url & request,
const parsed_url & base )
nodiscard

Definition at line 521 of file hardened_transport.cpp.

521 {
522 if (request.scheme != base.scheme || request.host != base.host
523 || request.port != base.port) {
524 return false;
525 }
526 if (base.path == "/") {
527 return true;
528 }
529 if (!request.path.starts_with(base.path)) {
530 return false;
531 }
532 return base.path.ends_with('/')
533 || request.path.size() == base.path.size()
534 || request.path.at(base.path.size()) == '/';
535 }

References arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parsed_url::host, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parsed_url::path, arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parsed_url::port, and arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::parsed_url::scheme.

Referenced by arachne::pheidippides::hardened_transport::implementation::execute().

Here is the caller graph for this function:

◆ write_cache_entry()

void arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::write_cache_entry ( const fs::path & cache_root,
const std::string & cache_key,
const acquired_artifact_v1 & acquired )

Definition at line 845 of file hardened_transport.cpp.

848 {
849 const auto stored_unix
850 = std::chrono::duration_cast<std::chrono::seconds>(
851 std::chrono::system_clock::now().time_since_epoch()
852 )
853 .count();
854 const nlohmann::ordered_json document {
855 { "cache_version", 1 },
856 { "cache_key", cache_key },
857 { "stored_unix", stored_unix },
858 { "artifact_ref", acquired.artifact_ref },
859 { "sha256", acquired.sha256 },
860 { "byte_count", acquired.byte_count },
861 { "http_status", acquired.http_status },
862 };
863 const std::string bytes
865 const auto sequence
866 = cache_stage_sequence.fetch_add(1U, std::memory_order_relaxed);
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(
872 temporary.c_str(),
873 O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600
874 );
875 if (descriptor < 0) {
876 return;
877 }
878 std::size_t offset = 0;
879 bool written = true;
880 while (offset < bytes.size()) {
881 const ssize_t count = ::write(
882 descriptor, bytes.data() + offset, bytes.size() - offset
883 );
884 if (count < 0 && errno == EINTR) {
885 continue;
886 }
887 if (count <= 0) {
888 written = false;
889 break;
890 }
891 offset += static_cast<std::size_t>(count);
892 }
893 if (written && ::fsync(descriptor) != 0) {
894 written = false;
895 }
896 if (::close(descriptor) != 0) {
897 written = false;
898 }
899 if (!written) {
900 std::error_code ignored;
901 fs::remove(temporary, ignored);
902 return;
903 }
904 std::error_code error;
905 fs::rename(temporary, target, error);
906 if (error) {
907 std::error_code ignored;
908 fs::remove(temporary, ignored);
909 }
910 }

References arachne::pheidippides::acquired_artifact_v1::artifact_ref, arachne::pheidippides::acquired_artifact_v1::byte_count, cache_stage_sequence, arachne::pheidippides::acquired_artifact_v1::http_status, and arachne::pheidippides::acquired_artifact_v1::sha256.

Variable Documentation

◆ cache_stage_sequence

std::atomic< std::uint64_t > arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::cache_stage_sequence { 0 }

Definition at line 42 of file hardened_transport.cpp.

42{ 0 };

Referenced by write_cache_entry().

◆ maximum_cache_document_bytes

std::uint64_t arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::maximum_cache_document_bytes = 64U * 1024U
constexpr

Definition at line 41 of file hardened_transport.cpp.

Referenced by read_cache_entry().

◆ maximum_cache_seconds

std::uint64_t arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::maximum_cache_seconds = 31'536'000U
constexpr

Definition at line 40 of file hardened_transport.cpp.

◆ maximum_policy_milliseconds

std::uint64_t arachne::pheidippides::anonymous_namespace{hardened_transport.cpp}::maximum_policy_milliseconds = 86'400'000U
constexpr

Definition at line 39 of file hardened_transport.cpp.