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

Classes

struct  curl_easy_deleter
struct  curl_url_deleter
struct  curl_slist_deleter
struct  parsed_url
struct  allowed_authority
class  verified_body_file
class  staging_file
struct  receive_context
struct  progress_context

Typedefs

using easy_handle = std::unique_ptr<CURL, curl_easy_deleter>
using url_handle = std::unique_ptr<CURLU, curl_url_deleter>
using header_list = std::unique_ptr<curl_slist, curl_slist_deleter>

Functions

void curl_share_lock (CURL *, const curl_lock_data, const curl_lock_access, void *) noexcept
void curl_share_unlock (CURL *, const curl_lock_data, void *) noexcept
void initialize_curl ()
bool is_control (const char character) noexcept
bool valid_request_id (const std::string_view request_id)
std::string artifact_id_for (const std::string_view request_id)
bool valid_header_name (const std::string_view name)
std::string ascii_lower (std::string value)
bool valid_header (const http_header &header)
std::optional< std::string > normalize_host (const std::string_view input)
std::optional< unsigned > parse_port (const std::string_view text) noexcept
std::optional< parsed_urlparse_url (const std::string &value, std::string &error)
std::optional< allowed_authorityparse_allowed_authority (const std::string_view input)
bool host_allowed (const parsed_url &url, const std::vector< std::string > &allowed_hosts, std::string &error)
std::optional< std::string > resolve_redirect (const std::string &base, const std::string &location, std::string &error)
bool path_begins_with (const std::filesystem::path &path, const std::filesystem::path &root)
bool ensure_safe_parent (const std::filesystem::path &root, const std::filesystem::path &target, std::string &error)
bool ensure_safe_existing_file (const std::filesystem::path &root, const std::filesystem::path &candidate, bool &unsafe, std::string &error)
std::size_t body_read_callback (char *destination, const std::size_t size, const std::size_t count, void *user_data) noexcept
int body_seek_callback (void *user_data, const curl_off_t offset, const int origin) noexcept
int transfer_progress_callback (void *user_data, const curl_off_t download_total, const curl_off_t download_now, const curl_off_t upload_total, const curl_off_t upload_now) noexcept
bool write_all (const int descriptor, const char *data, std::size_t size, std::string &error) noexcept
std::size_t body_callback (char *contents, const std::size_t size, const std::size_t count, void *user_data) noexcept
std::string_view trim_header_value (std::string_view value) noexcept
std::size_t response_header_callback (char *contents, const std::size_t size, const std::size_t count, void *user_data) noexcept
bool is_redirect_status (const long status) noexcept
bool is_retryable_status (const long status) noexcept
bool is_retryable_curl_status (const CURLcode status) noexcept
std::optional< std::chrono::milliseconds > retry_after_delay (const std::vector< http_header > &headers)
std::chrono::milliseconds retry_delay (const fetch_request_v1 &request, const std::size_t attempt, const std::vector< http_header > &headers)
long timeout_milliseconds (const std::chrono::steady_clock::time_point deadline) noexcept
header_list build_request_headers (const std::vector< http_header > &headers)
acquired_artifact_v1 fail_result (acquired_artifact_v1 result, const transport_status status, std::string message)
std::string validation_message (const arachnespace::contracts::validation_result &validation)
std::string rfc3339 (const std::chrono::system_clock::time_point time)
std::string_view status_name (const transport_status status) noexcept
std::optional< std::string > response_content_type (const std::vector< http_header > &headers)
bool sensitive_header_name (const std::string_view name)
int hexadecimal_value (const char character) noexcept
std::string decode_query_name (const std::string_view name)
bool sensitive_query_name (std::string name)
std::string redact_url_query (const std::string &url)
std::string receipt_safe_text (const std::string_view value)

Variables

constexpr std::size_t maximum_header_bytes = 1024U * 1024U
constexpr std::size_t maximum_response_headers = 1'024U
constexpr std::size_t maximum_redirects = 20U
constexpr std::size_t maximum_transport_attempts = 20U
constexpr std::size_t maximum_url_bytes = 64U * 1024U
constexpr std::size_t maximum_request_id_bytes = 128U
std::once_flag curl_global_once
std::recursive_mutex curl_share_mutex
CURLSH * curl_connection_share = nullptr
std::atomic< std::uint64_t > stage_sequence { 0 }

Typedef Documentation

◆ easy_handle

typedef std::unique_ptr< CURL, curl_easy_deleter > arachne::pheidippides::anonymous_namespace{transport.cpp}::easy_handle = std::unique_ptr<CURL, curl_easy_deleter>

Definition at line 77 of file transport.cpp.

◆ header_list

typedef std::unique_ptr< curl_slist, curl_slist_deleter > arachne::pheidippides::anonymous_namespace{transport.cpp}::header_list = std::unique_ptr<curl_slist, curl_slist_deleter>

Definition at line 79 of file transport.cpp.

◆ url_handle

typedef std::unique_ptr< CURLU, curl_url_deleter > arachne::pheidippides::anonymous_namespace{transport.cpp}::url_handle = std::unique_ptr<CURLU, curl_url_deleter>

Definition at line 78 of file transport.cpp.

Function Documentation

◆ artifact_id_for()

std::string arachne::pheidippides::anonymous_namespace{transport.cpp}::artifact_id_for ( const std::string_view request_id)
nodiscard

Definition at line 130 of file transport.cpp.

130 {
131 constexpr std::string_view suffix = ".artifact";
132 if (request_id.size() + suffix.size() <= maximum_request_id_bytes) {
133 return std::string(request_id) + std::string(suffix);
134 }
135 return "artifact-" + crypto::sha256(request_id).substr(0, 32U);
136 }
std::string sha256(std::span< const std::byte > bytes)
Definition crypto.cpp:209

References maximum_request_id_bytes, and arachne::crypto::sha256().

Here is the call graph for this function:

◆ ascii_lower()

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

Definition at line 149 of file transport.cpp.

149 {
150 std::ranges::transform(value, value.begin(), [](const char character) {
151 return static_cast<char>(
152 std::tolower(static_cast<unsigned char>(character))
153 );
154 });
155 return value;
156 }

Referenced by response_header_callback(), and sensitive_header_name().

Here is the caller graph for this function:

◆ body_callback()

std::size_t arachne::pheidippides::anonymous_namespace{transport.cpp}::body_callback ( char * contents,
const std::size_t size,
const std::size_t count,
void * user_data )
noexcept

Definition at line 869 of file transport.cpp.

872 {
873 auto& context = *static_cast<receive_context*>(user_data);
874 if (size != 0
875 && count > std::numeric_limits<std::size_t>::max() / size) {
876 context.too_large = true;
877 return 0;
878 }
879 const std::size_t total = size * count;
880 if (total > context.max_bytes
881 || context.byte_count > context.max_bytes - total) {
882 context.too_large = true;
883 return 0;
884 }
885 if (!write_all(
886 context.descriptor, contents, total, context.write_error
887 )) {
888 context.write_failed = true;
889 return 0;
890 }
891 try {
892 context.hasher.update(std::as_bytes(std::span { contents, total }));
893 } catch (const std::exception& exception) {
894 context.write_failed = true;
895 context.write_error = exception.what();
896 return 0;
897 }
898 context.byte_count += total;
899 return total;
900 }
bool write_all(const int descriptor, const char *data, std::size_t size, std::string &error) noexcept

References arachne::pheidippides::anonymous_namespace{transport.cpp}::receive_context::byte_count, arachne::pheidippides::anonymous_namespace{transport.cpp}::receive_context::descriptor, arachne::pheidippides::anonymous_namespace{transport.cpp}::receive_context::max_bytes, arachne::pheidippides::anonymous_namespace{transport.cpp}::receive_context::too_large, write_all(), arachne::pheidippides::anonymous_namespace{transport.cpp}::receive_context::write_error, and arachne::pheidippides::anonymous_namespace{transport.cpp}::receive_context::write_failed.

Referenced by arachne::pheidippides::transport::execute().

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

◆ body_read_callback()

std::size_t arachne::pheidippides::anonymous_namespace{transport.cpp}::body_read_callback ( char * destination,
const std::size_t size,
const std::size_t count,
void * user_data )
noexcept

Definition at line 661 of file transport.cpp.

664 {
665 auto& body = *static_cast<verified_body_file*>(user_data);
666 if (size != 0
667 && count > std::numeric_limits<std::size_t>::max() / size) {
668 return CURL_READFUNC_ABORT;
669 }
670 return body.read(destination, size * count);
671 }
std::size_t read(char *destination, const std::size_t size) noexcept

References arachne::pheidippides::anonymous_namespace{transport.cpp}::verified_body_file::read().

Referenced by arachne::pheidippides::transport::execute().

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

◆ body_seek_callback()

int arachne::pheidippides::anonymous_namespace{transport.cpp}::body_seek_callback ( void * user_data,
const curl_off_t offset,
const int origin )
noexcept

Definition at line 673 of file transport.cpp.

675 {
676 return static_cast<verified_body_file*>(user_data)->seek(
677 offset, origin
678 );
679 }

References arachne::pheidippides::anonymous_namespace{transport.cpp}::verified_body_file::seek().

Referenced by arachne::pheidippides::transport::execute().

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

◆ build_request_headers()

header_list arachne::pheidippides::anonymous_namespace{transport.cpp}::build_request_headers ( const std::vector< http_header > & headers)
nodiscard

Definition at line 1081 of file transport.cpp.

1081 {
1082 curl_slist* raw = nullptr;
1083 for (const auto& header : headers) {
1084 const std::string line = header.name + ":" + header.value;
1085 curl_slist* appended = curl_slist_append(raw, line.c_str());
1086 if (appended == nullptr) {
1087 curl_slist_free_all(raw);
1088 throw std::bad_alloc();
1089 }
1090 raw = appended;
1091 }
1092 return header_list(raw);
1093 }
std::unique_ptr< curl_slist, curl_slist_deleter > header_list
Definition transport.cpp:79

◆ curl_share_lock()

void arachne::pheidippides::anonymous_namespace{transport.cpp}::curl_share_lock ( CURL * ,
const curl_lock_data ,
const curl_lock_access ,
void *  )
noexcept

Definition at line 49 of file transport.cpp.

51 {
52 curl_share_mutex.lock();
53 }

References curl_share_mutex.

Referenced by initialize_curl().

Here is the caller graph for this function:

◆ curl_share_unlock()

void arachne::pheidippides::anonymous_namespace{transport.cpp}::curl_share_unlock ( CURL * ,
const curl_lock_data ,
void *  )
noexcept

Definition at line 55 of file transport.cpp.

55 {
56 curl_share_mutex.unlock();
57 }

References curl_share_mutex.

Referenced by initialize_curl().

Here is the caller graph for this function:

◆ decode_query_name()

std::string arachne::pheidippides::anonymous_namespace{transport.cpp}::decode_query_name ( const std::string_view name)
nodiscard

Definition at line 1205 of file transport.cpp.

1205 {
1206 std::string result;
1207 result.reserve(name.size());
1208 for (std::size_t index = 0; index < name.size(); ++index) {
1209 if (name[index] == '%' && index + 2U < name.size()) {
1210 const int high = hexadecimal_value(name[index + 1U]);
1211 const int low = hexadecimal_value(name[index + 2U]);
1212 if (high >= 0 && low >= 0) {
1213 result.push_back(static_cast<char>(high * 16 + low));
1214 index += 2U;
1215 continue;
1216 }
1217 }
1218 result.push_back(name[index] == '+' ? ' ' : name[index]);
1219 }
1220 return result;
1221 }
int hexadecimal_value(const char character) noexcept

◆ ensure_safe_existing_file()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::ensure_safe_existing_file ( const std::filesystem::path & root,
const std::filesystem::path & candidate,
bool & unsafe,
std::string & error )
nodiscard

Definition at line 447 of file transport.cpp.

450 {
451 unsafe = false;
452 if (!path_begins_with(candidate, root)) {
453 unsafe = true;
454 error = "body artifact escapes the configured root";
455 return false;
456 }
457 const std::filesystem::path relative
458 = candidate.lexically_relative(root);
459 std::filesystem::path current = root;
460 std::size_t component_index = 0;
461 const std::size_t component_count = static_cast<std::size_t>(
462 std::distance(relative.begin(), relative.end())
463 );
464 for (const auto& component : relative) {
465 ++component_index;
466 current /= component;
467 std::error_code ec;
468 const auto state = std::filesystem::symlink_status(current, ec);
469 if (ec || !std::filesystem::exists(state)) {
470 error = "body artifact is missing or cannot be inspected";
471 return false;
472 }
473 if (std::filesystem::is_symlink(state)) {
474 unsafe = true;
475 error = "body artifact path contains a symlink";
476 return false;
477 }
478 const bool final_component = component_index == component_count;
479 if ((!final_component && !std::filesystem::is_directory(state))
480 || (final_component
481 && !std::filesystem::is_regular_file(state))) {
482 error = "body artifact path has an unexpected file type";
483 return false;
484 }
485 }
486 return component_count != 0U;
487 }
bool path_begins_with(const std::filesystem::path &path, const std::filesystem::path &root)

References path_begins_with().

Referenced by arachne::pheidippides::anonymous_namespace{transport.cpp}::verified_body_file::open_and_verify().

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

◆ ensure_safe_parent()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::ensure_safe_parent ( const std::filesystem::path & root,
const std::filesystem::path & target,
std::string & error )
nodiscard

Definition at line 399 of file transport.cpp.

402 {
403 const std::filesystem::path parent = target.parent_path();
404 if (!path_begins_with(parent, root)) {
405 error = "artifact parent escapes the configured root";
406 return false;
407 }
408 const std::filesystem::path relative = parent.lexically_relative(root);
409 std::filesystem::path current = root;
410 for (const auto& component : relative) {
411 if (component == ".") {
412 continue;
413 }
414 current /= component;
415 std::error_code ec;
416 const auto state = std::filesystem::symlink_status(current, ec);
417 if (ec && ec != std::errc::no_such_file_or_directory) {
418 error = "cannot inspect artifact directory: " + ec.message();
419 return false;
420 }
421 if (!ec && std::filesystem::exists(state)) {
422 if (std::filesystem::is_symlink(state)
423 || !std::filesystem::is_directory(state)) {
424 error = "artifact directory component is not a real "
425 "directory";
426 return false;
427 }
428 continue;
429 }
430 ec.clear();
431 if (!std::filesystem::create_directory(current, ec) && ec) {
432 error = "cannot create artifact directory: " + ec.message();
433 return false;
434 }
435 }
436
437 std::error_code ec;
438 const auto canonical_parent
439 = std::filesystem::weakly_canonical(parent, ec);
440 if (ec || !path_begins_with(canonical_parent, root)) {
441 error = "artifact directory resolves outside the configured root";
442 return false;
443 }
444 return true;
445 }

References path_begins_with().

Here is the call graph for this function:

◆ fail_result()

acquired_artifact_v1 arachne::pheidippides::anonymous_namespace{transport.cpp}::fail_result ( acquired_artifact_v1 result,
const transport_status status,
std::string message )
nodiscard

Definition at line 1095 of file transport.cpp.

1098 {
1099 result.status = status;
1100 result.error_message = std::move(message);
1101 result.completed_at = std::chrono::system_clock::now();
1102 return result;
1103 }
std::chrono::system_clock::time_point completed_at

References arachne::pheidippides::acquired_artifact_v1::error_message, and arachne::pheidippides::acquired_artifact_v1::status.

◆ hexadecimal_value()

int arachne::pheidippides::anonymous_namespace{transport.cpp}::hexadecimal_value ( const char character)
nodiscardnoexcept

Definition at line 1192 of file transport.cpp.

1192 {
1193 if (character >= '0' && character <= '9') {
1194 return character - '0';
1195 }
1196 if (character >= 'a' && character <= 'f') {
1197 return character - 'a' + 10;
1198 }
1199 if (character >= 'A' && character <= 'F') {
1200 return character - 'A' + 10;
1201 }
1202 return -1;
1203 }

◆ host_allowed()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::host_allowed ( const parsed_url & url,
const std::vector< std::string > & allowed_hosts,
std::string & error )
nodiscard

Definition at line 328 of file transport.cpp.

331 {
332 if (allowed_hosts.empty()) {
333 error = "allowed_hosts must contain at least one exact host";
334 return false;
335 }
336 bool matched = false;
337 const unsigned effective_port
338 = url.port.value_or(url.scheme == "https" ? 443U : 80U);
339 for (const std::string& candidate : allowed_hosts) {
340 auto allowed = parse_allowed_authority(candidate);
341 if (!allowed) {
342 error = "allowed_hosts contains an invalid host";
343 return false;
344 }
345 if (allowed->host == url.host
346 && (!allowed->port || *allowed->port == effective_port)) {
347 matched = true;
348 }
349 }
350 if (matched) {
351 return true;
352 }
353 error = "URL host is not permitted by allowed_hosts";
354 return false;
355 }
std::optional< allowed_authority > parse_allowed_authority(const std::string_view input)

References arachne::pheidippides::anonymous_namespace{transport.cpp}::parsed_url::scheme.

◆ initialize_curl()

void arachne::pheidippides::anonymous_namespace{transport.cpp}::initialize_curl ( )

Definition at line 81 of file transport.cpp.

81 {
82 std::call_once(curl_global_once, [] {
83 const CURLcode result = curl_global_init(CURL_GLOBAL_DEFAULT);
84 if (result != CURLE_OK) {
85 throw std::runtime_error("curl_global_init failed");
86 }
87 curl_connection_share = curl_share_init();
88 if (curl_connection_share == nullptr
89 || curl_share_setopt(
90 curl_connection_share, CURLSHOPT_LOCKFUNC,
92 ) != CURLSHE_OK
93 || curl_share_setopt(
94 curl_connection_share, CURLSHOPT_UNLOCKFUNC,
96 ) != CURLSHE_OK
97 || curl_share_setopt(
98 curl_connection_share, CURLSHOPT_SHARE,
99 CURL_LOCK_DATA_DNS
100 ) != CURLSHE_OK
101 || curl_share_setopt(
102 curl_connection_share, CURLSHOPT_SHARE,
103 CURL_LOCK_DATA_CONNECT
104 ) != CURLSHE_OK) {
105 throw std::runtime_error(
106 "cannot initialize the Pheidippides connection pool"
107 );
108 }
109 });
110 }
void curl_share_lock(CURL *, const curl_lock_data, const curl_lock_access, void *) noexcept
Definition transport.cpp:49
void curl_share_unlock(CURL *, const curl_lock_data, void *) noexcept
Definition transport.cpp:55

References curl_connection_share, curl_global_once, curl_share_lock(), and curl_share_unlock().

Referenced by arachne::pheidippides::transport::transport().

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

◆ is_control()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::is_control ( const char character)
nodiscardnoexcept

Definition at line 112 of file transport.cpp.

112 {
113 const auto value = static_cast<unsigned char>(character);
114 return value < 0x20U || value == 0x7fU;
115 }

◆ is_redirect_status()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::is_redirect_status ( const long status)
nodiscardnoexcept

Definition at line 958 of file transport.cpp.

958 {
959 switch (status) {
960 case 300:
961 case 301:
962 case 302:
963 case 303:
964 case 305:
965 case 307:
966 case 308:
967 return true;
968 default:
969 return false;
970 }
971 }

Referenced by arachne::pheidippides::transport::execute().

Here is the caller graph for this function:

◆ is_retryable_curl_status()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::is_retryable_curl_status ( const CURLcode status)
nodiscardnoexcept

Definition at line 979 of file transport.cpp.

979 {
980 switch (status) {
981 case CURLE_COULDNT_RESOLVE_PROXY:
982 case CURLE_COULDNT_RESOLVE_HOST:
983 case CURLE_COULDNT_CONNECT:
984 case CURLE_PARTIAL_FILE:
985 case CURLE_HTTP2:
986 case CURLE_WRITE_ERROR:
987 case CURLE_UPLOAD_FAILED:
988 case CURLE_READ_ERROR:
989 case CURLE_OPERATION_TIMEDOUT:
990 case CURLE_ABORTED_BY_CALLBACK:
991 case CURLE_HTTP_POST_ERROR:
992 case CURLE_RECV_ERROR:
993 case CURLE_SEND_ERROR:
994 case CURLE_GOT_NOTHING:
995 case CURLE_AGAIN:
996#ifdef CURLE_HTTP3
997 case CURLE_HTTP3:
998#endif
999 return true;
1000 default:
1001 return false;
1002 }
1003 }

Referenced by arachne::pheidippides::transport::execute().

Here is the caller graph for this function:

◆ is_retryable_status()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::is_retryable_status ( const long status)
nodiscardnoexcept

Definition at line 973 of file transport.cpp.

973 {
974 return status == 408 || status == 425 || status == 429 || status == 500
975 || status == 502 || status == 503 || status == 504;
976 }

Referenced by arachne::pheidippides::transport::execute().

Here is the caller graph for this function:

◆ normalize_host()

std::optional< std::string > arachne::pheidippides::anonymous_namespace{transport.cpp}::normalize_host ( const std::string_view input)
nodiscard

Definition at line 168 of file transport.cpp.

168 {
169 if (input.empty() || input.find('\0') != std::string_view::npos
170 || std::ranges::any_of(input, [](const char character) {
171 return std::isspace(static_cast<unsigned char>(character))
172 != 0;
173 })) {
174 return std::nullopt;
175 }
176 std::string host(input);
177 if (host.size() >= 2U && host.front() == '[' && host.back() == ']') {
178 host = host.substr(1U, host.size() - 2U);
179 }
180 if (!host.empty() && host.back() == '.') {
181 host.pop_back();
182 }
183 if (host.empty() || host.find_first_of("/@?#") != std::string::npos
184 || std::ranges::count(host, ':') == 1) {
185 return std::nullopt;
186 }
187 return ascii_lower(std::move(host));
188 }

◆ parse_allowed_authority()

std::optional< allowed_authority > arachne::pheidippides::anonymous_namespace{transport.cpp}::parse_allowed_authority ( const std::string_view input)
nodiscard

Definition at line 292 of file transport.cpp.

292 {
293 std::string_view host = input;
294 std::optional<unsigned> port;
295 if (input.starts_with('[')) {
296 const std::size_t closing = input.find(']');
297 if (closing == std::string_view::npos) {
298 return std::nullopt;
299 }
300 host = input.substr(0, closing + 1U);
301 if (closing + 1U != input.size()) {
302 if (input[closing + 1U] != ':') {
303 return std::nullopt;
304 }
305 port = parse_port(input.substr(closing + 2U));
306 if (!port) {
307 return std::nullopt;
308 }
309 }
310 } else if (std::ranges::count(input, ':') == 1) {
311 const std::size_t colon = input.find(':');
312 host = input.substr(0, colon);
313 port = parse_port(input.substr(colon + 1U));
314 if (!port) {
315 return std::nullopt;
316 }
317 }
318 auto normalized = normalize_host(host);
319 if (!normalized) {
320 return std::nullopt;
321 }
322 return allowed_authority {
323 .host = std::move(*normalized),
324 .port = port,
325 };
326 }
std::optional< std::string > normalize_host(const std::string_view input)
std::optional< unsigned > parse_port(const std::string_view text) noexcept

◆ parse_port()

std::optional< unsigned > arachne::pheidippides::anonymous_namespace{transport.cpp}::parse_port ( const std::string_view text)
nodiscardnoexcept

Definition at line 198 of file transport.cpp.

198 {
199 if (text.empty()
200 || !std::ranges::all_of(text, [](const char character) {
201 return character >= '0' && character <= '9';
202 })) {
203 return std::nullopt;
204 }
205 unsigned value = 0;
206 for (const char character : text) {
207 value = value * 10U
208 + static_cast<unsigned>(character - static_cast<char>('0'));
209 if (value > 65'535U) {
210 return std::nullopt;
211 }
212 }
213 return value == 0U ? std::nullopt : std::optional<unsigned> { value };
214 }

◆ parse_url()

std::optional< parsed_url > arachne::pheidippides::anonymous_namespace{transport.cpp}::parse_url ( const std::string & value,
std::string & error )
nodiscard

Definition at line 217 of file transport.cpp.

217 {
218 if (value.empty() || value.size() > maximum_url_bytes
219 || value.find('\0') != std::string::npos) {
220 error = "URL is empty, contains NUL, or exceeds the length limit";
221 return std::nullopt;
222 }
223
224 url_handle parsed(curl_url());
225 if (!parsed
226 || curl_url_set(parsed.get(), CURLUPART_URL, value.c_str(), 0)
227 != CURLUE_OK) {
228 error = "URL is not an absolute HTTP or HTTPS URL";
229 return std::nullopt;
230 }
231
232 auto get_part
233 = [&](const CURLUPart part) -> std::optional<std::string> {
234 char* raw = nullptr;
235 const CURLUcode code
236 = curl_url_get(parsed.get(), part, &raw, CURLU_NO_DEFAULT_PORT);
237 if (code != CURLUE_OK || raw == nullptr) {
238 return std::nullopt;
239 }
240 std::unique_ptr<char, decltype(&curl_free)> owned(raw, &curl_free);
241 return std::string(raw);
242 };
243
244 auto scheme = get_part(CURLUPART_SCHEME);
245 auto host = get_part(CURLUPART_HOST);
246 auto port_text = get_part(CURLUPART_PORT);
247 auto normalized = get_part(CURLUPART_URL);
248 if (!scheme || !host || !normalized) {
249 error = "URL must include a scheme and host";
250 return std::nullopt;
251 }
252 *scheme = ascii_lower(std::move(*scheme));
253 if (*scheme != "http" && *scheme != "https") {
254 error = "only HTTP and HTTPS URLs are allowed";
255 return std::nullopt;
256 }
257 auto normalized_host = normalize_host(*host);
258 if (!normalized_host) {
259 error = "URL host is invalid";
260 return std::nullopt;
261 }
262 std::optional<unsigned> port;
263 if (port_text) {
264 port = parse_port(*port_text);
265 if (!port) {
266 error = "URL port is invalid";
267 return std::nullopt;
268 }
269 }
270 if (get_part(CURLUPART_USER) || get_part(CURLUPART_PASSWORD)) {
271 error = "URL credentials are not allowed";
272 return std::nullopt;
273 }
274 if (get_part(CURLUPART_FRAGMENT)) {
275 error = "URL fragments are not allowed";
276 return std::nullopt;
277 }
278 return parsed_url {
279 .normalized_url = std::move(*normalized),
280 .scheme = std::move(*scheme),
281 .host = std::move(*normalized_host),
282 .port = port,
283 };
284 }
std::unique_ptr< CURLU, curl_url_deleter > url_handle
Definition transport.cpp:78

◆ path_begins_with()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::path_begins_with ( const std::filesystem::path & path,
const std::filesystem::path & root )
nodiscard

Definition at line 385 of file transport.cpp.

387 {
388 auto path_iterator = path.begin();
389 for (auto root_iterator = root.begin(); root_iterator != root.end();
390 ++root_iterator, ++path_iterator) {
391 if (path_iterator == path.end()
392 || *path_iterator != *root_iterator) {
393 return false;
394 }
395 }
396 return true;
397 }

Referenced by ensure_safe_existing_file(), and ensure_safe_parent().

Here is the caller graph for this function:

◆ receipt_safe_text()

std::string arachne::pheidippides::anonymous_namespace{transport.cpp}::receipt_safe_text ( const std::string_view value)
nodiscard

Definition at line 1274 of file transport.cpp.

1274 {
1275 constexpr std::string_view hexadecimal = "0123456789ABCDEF";
1276 std::string result;
1277 result.reserve(value.size());
1278 for (const char raw_character : value) {
1279 const auto character = static_cast<unsigned char>(raw_character);
1280 if (character >= 0x20U && character <= 0x7eU) {
1281 result.push_back(static_cast<char>(character));
1282 } else {
1283 result.push_back('%');
1284 result.push_back(hexadecimal[character >> 4U]);
1285 result.push_back(hexadecimal[character & 0x0fU]);
1286 }
1287 }
1288 return result;
1289 }

◆ redact_url_query()

std::string arachne::pheidippides::anonymous_namespace{transport.cpp}::redact_url_query ( const std::string & url)
nodiscard

Definition at line 1233 of file transport.cpp.

1233 {
1234 const std::size_t question = url.find('?');
1235 if (question == std::string::npos) {
1236 return url;
1237 }
1238 const std::size_t fragment = url.find('#', question + 1U);
1239 const std::size_t query_end
1240 = fragment == std::string::npos ? url.size() : fragment;
1241 std::string result = url.substr(0, question + 1U);
1242 std::string_view query(
1243 url.data() + question + 1U, query_end - question - 1U
1244 );
1245 bool first = true;
1246 while (!query.empty()) {
1247 const std::size_t ampersand = query.find('&');
1248 const std::string_view parameter = query.substr(0, ampersand);
1249 const std::size_t equals = parameter.find('=');
1250 const std::string name(parameter.substr(0, equals));
1251 if (!first) {
1252 result.push_back('&');
1253 }
1254 first = false;
1255 result += name;
1256 if (equals != std::string_view::npos) {
1257 result.push_back('=');
1258 result += sensitive_query_name(name)
1259 ? "%5BREDACTED%5D"
1260 : std::string(parameter.substr(equals + 1U));
1261 }
1262 if (ampersand == std::string_view::npos) {
1263 query = {};
1264 } else {
1265 query.remove_prefix(ampersand + 1U);
1266 }
1267 }
1268 if (fragment != std::string::npos) {
1269 result += url.substr(fragment);
1270 }
1271 return result;
1272 }

References sensitive_query_name().

Here is the call graph for this function:

◆ resolve_redirect()

std::optional< std::string > arachne::pheidippides::anonymous_namespace{transport.cpp}::resolve_redirect ( const std::string & base,
const std::string & location,
std::string & error )
nodiscard

Definition at line 357 of file transport.cpp.

359 {
360 if (location.empty() || location.find('\0') != std::string::npos) {
361 error = "redirect Location is empty or invalid";
362 return std::nullopt;
363 }
364 url_handle resolved(curl_url());
365 if (!resolved
366 || curl_url_set(resolved.get(), CURLUPART_URL, base.c_str(), 0)
367 != CURLUE_OK
368 || curl_url_set(resolved.get(), CURLUPART_URL, location.c_str(), 0)
369 != CURLUE_OK) {
370 error = "redirect Location could not be resolved";
371 return std::nullopt;
372 }
373 char* raw = nullptr;
374 if (curl_url_get(
375 resolved.get(), CURLUPART_URL, &raw, CURLU_NO_DEFAULT_PORT
376 ) != CURLUE_OK
377 || raw == nullptr) {
378 error = "redirect URL could not be serialized";
379 return std::nullopt;
380 }
381 std::unique_ptr<char, decltype(&curl_free)> owned(raw, &curl_free);
382 return std::string(raw);
383 }

◆ response_content_type()

std::optional< std::string > arachne::pheidippides::anonymous_namespace{transport.cpp}::response_content_type ( const std::vector< http_header > & headers)
nodiscard

Definition at line 1173 of file transport.cpp.

1173 {
1174 for (const auto& header : headers) {
1175 if (ascii_lower(header.name) == "content-type"
1176 && !header.value.empty()) {
1177 return header.value;
1178 }
1179 }
1180 return std::nullopt;
1181 }

◆ response_header_callback()

std::size_t arachne::pheidippides::anonymous_namespace{transport.cpp}::response_header_callback ( char * contents,
const std::size_t size,
const std::size_t count,
void * user_data )
noexcept

Definition at line 915 of file transport.cpp.

918 {
919 auto& context = *static_cast<receive_context*>(user_data);
920 if (size != 0
921 && count > std::numeric_limits<std::size_t>::max() / size) {
922 context.headers_too_large = true;
923 return 0;
924 }
925 const std::size_t total = size * count;
926 if (total > maximum_header_bytes
927 || context.header_bytes > maximum_header_bytes - total) {
928 context.headers_too_large = true;
929 return 0;
930 }
931 context.header_bytes += total;
932 const std::string_view line(contents, total);
933 if (line.starts_with("HTTP/")) {
934 context.headers.clear();
935 context.location.clear();
936 return total;
937 }
938 const std::size_t colon = line.find(':');
939 if (colon == std::string_view::npos) {
940 return total;
941 }
942 http_header header {
943 .name = std::string(line.substr(0, colon)),
944 .value = std::string(trim_header_value(line.substr(colon + 1U))),
945 };
946 if (!valid_header_name(header.name)) {
947 return total;
948 }
949 if (ascii_lower(header.name) == "location") {
950 context.location = header.value;
951 }
952 if (context.headers.size() < maximum_response_headers) {
953 context.headers.emplace_back(std::move(header));
954 }
955 return total;
956 }
bool valid_header_name(const std::string_view name)
std::string_view trim_header_value(std::string_view value) noexcept

References ascii_lower(), arachne::pheidippides::anonymous_namespace{transport.cpp}::receive_context::header_bytes, arachne::pheidippides::anonymous_namespace{transport.cpp}::receive_context::headers_too_large, arachne::pheidippides::anonymous_namespace{transport.cpp}::receive_context::location, maximum_header_bytes, maximum_response_headers, arachne::pheidippides::http_header::name, and arachne::pheidippides::http_header::value.

Referenced by arachne::pheidippides::transport::execute().

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

◆ retry_after_delay()

std::optional< std::chrono::milliseconds > arachne::pheidippides::anonymous_namespace{transport.cpp}::retry_after_delay ( const std::vector< http_header > & headers)
nodiscard

Definition at line 1006 of file transport.cpp.

1006 {
1007 for (const auto& header : headers) {
1008 if (ascii_lower(header.name) != "retry-after") {
1009 continue;
1010 }
1011 std::uint64_t seconds = 0;
1012 const char* begin = header.value.data();
1013 const char* end = begin + header.value.size();
1014 const auto parsed = std::from_chars(begin, end, seconds);
1015 if (parsed.ec == std::errc {} && parsed.ptr == end) {
1016 constexpr std::uint64_t maximum_seconds = 3'600U;
1017 return std::chrono::milliseconds(
1018 std::min(seconds, maximum_seconds) * 1'000U
1019 );
1020 }
1021 const std::time_t date
1022 = curl_getdate(header.value.c_str(), nullptr);
1023 if (date >= 0) {
1024 const std::time_t now = std::time(nullptr);
1025 const auto delay = std::max<std::time_t>(0, date - now);
1026 return std::chrono::milliseconds(
1027 std::min<std::time_t>(delay, 3'600) * 1'000
1028 );
1029 }
1030 return std::nullopt;
1031 }
1032 return std::nullopt;
1033 }

◆ retry_delay()

std::chrono::milliseconds arachne::pheidippides::anonymous_namespace{transport.cpp}::retry_delay ( const fetch_request_v1 & request,
const std::size_t attempt,
const std::vector< http_header > & headers )
nodiscard

Definition at line 1035 of file transport.cpp.

1038 {
1039 if (request.respect_retry_after) {
1040 if (const auto provided = retry_after_delay(headers)) {
1041 return std::min(*provided, request.maximum_retry_delay);
1042 }
1043 }
1044 std::int64_t delay = request.initial_retry_delay.count();
1045 for (std::size_t power = 1; power < attempt; ++power) {
1046 if (delay >= request.maximum_retry_delay.count() / 2) {
1047 delay = request.maximum_retry_delay.count();
1048 break;
1049 }
1050 delay *= 2;
1051 }
1052 delay = std::min(delay, request.maximum_retry_delay.count());
1053 const std::string jitter_seed
1054 = request.request_id + ":" + std::to_string(attempt);
1055 const std::string digest = crypto::sha256(jitter_seed);
1056 unsigned jitter_value = 0;
1057 static_cast<void>(
1058 std::from_chars(digest.data(), digest.data() + 4, jitter_value, 16)
1059 );
1060 const std::int64_t per_mille
1061 = 800 + static_cast<std::int64_t>(jitter_value % 401U);
1062 return std::chrono::milliseconds(delay * per_mille / 1'000);
1063 }
std::optional< std::chrono::milliseconds > retry_after_delay(const std::vector< http_header > &headers)
std::chrono::milliseconds maximum_retry_delay
Definition transport.hpp:90
std::chrono::milliseconds initial_retry_delay
Definition transport.hpp:89

References arachne::pheidippides::fetch_request_v1::request_id, arachne::pheidippides::fetch_request_v1::respect_retry_after, and arachne::crypto::sha256().

Here is the call graph for this function:

◆ rfc3339()

std::string arachne::pheidippides::anonymous_namespace{transport.cpp}::rfc3339 ( const std::chrono::system_clock::time_point time)
nodiscard

Definition at line 1121 of file transport.cpp.

1121 {
1122 const std::time_t converted
1123 = std::chrono::system_clock::to_time_t(time);
1124 std::tm utc {};
1125 if (::gmtime_r(&converted, &utc) == nullptr) {
1126 throw std::runtime_error("cannot format transport timestamp");
1127 }
1128 std::ostringstream output;
1129 output << std::put_time(&utc, "%Y-%m-%dT%H:%M:%SZ");
1130 return output.str();
1131 }

◆ sensitive_header_name()

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

Definition at line 1183 of file transport.cpp.

1183 {
1184 const std::string lowered = ascii_lower(std::string(name));
1185 return lowered == "authorization" || lowered == "proxy-authorization"
1186 || lowered == "cookie" || lowered == "set-cookie"
1187 || lowered == "x-api-key" || lowered == "api-key"
1188 || lowered == "x-goog-api-key" || lowered == "x-auth-token"
1189 || lowered == "x-access-token" || lowered == "private-token";
1190 }

References ascii_lower().

Here is the call graph for this function:

◆ sensitive_query_name()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::sensitive_query_name ( std::string name)
nodiscard

Definition at line 1223 of file transport.cpp.

1223 {
1224 name = ascii_lower(decode_query_name(name));
1225 return name.contains("token") || name.contains("secret")
1226 || name.contains("password") || name.contains("signature")
1227 || name.contains("credential") || name.contains("api_key")
1228 || name.contains("apikey") || name.contains("access_key")
1229 || name == "key" || name == "auth" || name == "sig"
1230 || name.starts_with("x-amz-");
1231 }
std::string decode_query_name(const std::string_view name)

Referenced by redact_url_query().

Here is the caller graph for this function:

◆ status_name()

std::string_view arachne::pheidippides::anonymous_namespace{transport.cpp}::status_name ( const transport_status status)
nodiscardnoexcept

Definition at line 1134 of file transport.cpp.

1134 {
1135 switch (status) {
1137 return "delivered";
1139 return "invalid_request";
1141 return "unsafe_artifact_ref";
1143 return "disallowed_host";
1145 return "redirect_rejected";
1147 return "redirect_limit_exceeded";
1149 return "timed_out";
1151 return "response_too_large";
1153 return "network_error";
1155 return "storage_error";
1157 return "artifact_exists";
1159 return "door_policy_rejected";
1161 return "cache_miss";
1163 return "checksum_mismatch";
1165 return "retry_budget_exhausted";
1167 return "admission_timeout";
1168 }
1169 return "unknown_transport_status";
1170 }

References arachne::pheidippides::admission_timeout, arachne::pheidippides::artifact_exists, arachne::pheidippides::cache_miss, arachne::pheidippides::checksum_mismatch, arachne::pheidippides::delivered, arachne::pheidippides::disallowed_host, arachne::pheidippides::door_policy_rejected, arachne::pheidippides::invalid_request, arachne::pheidippides::network_error, arachne::pheidippides::redirect_limit_exceeded, arachne::pheidippides::redirect_rejected, arachne::pheidippides::response_too_large, arachne::pheidippides::retry_budget_exhausted, arachne::pheidippides::storage_error, arachne::pheidippides::timed_out, and arachne::pheidippides::unsafe_artifact_ref.

◆ timeout_milliseconds()

long arachne::pheidippides::anonymous_namespace{transport.cpp}::timeout_milliseconds ( const std::chrono::steady_clock::time_point deadline)
nodiscardnoexcept

Definition at line 1065 of file transport.cpp.

1067 {
1068 const auto remaining
1069 = std::chrono::duration_cast<std::chrono::milliseconds>(
1070 deadline - std::chrono::steady_clock::now()
1071 );
1072 if (remaining.count() <= 0) {
1073 return 0;
1074 }
1075 return static_cast<long>(std::min<std::int64_t>(
1076 remaining.count(), std::numeric_limits<long>::max()
1077 ));
1078 }

◆ transfer_progress_callback()

int arachne::pheidippides::anonymous_namespace{transport.cpp}::transfer_progress_callback ( void * user_data,
const curl_off_t download_total,
const curl_off_t download_now,
const curl_off_t upload_total,
const curl_off_t upload_now )
noexcept

Definition at line 812 of file transport.cpp.

816 {
817 auto& progress = *static_cast<progress_context*>(user_data);
818 const auto now = std::chrono::steady_clock::now();
819 if (download_now != progress.downloaded) {
820 progress.downloaded = download_now;
821 progress.last_download_progress = now;
822 }
823 if (upload_now != progress.uploaded) {
824 progress.uploaded = upload_now;
825 progress.last_upload_progress = now;
826 }
827 const bool download_complete
828 = download_total > 0 && download_now >= download_total;
829 const bool upload_complete = !progress.upload_expected
830 || (upload_total > 0 && upload_now >= upload_total);
831 if (upload_complete && !download_complete
832 && now - progress.last_download_progress > progress.read_timeout) {
833 progress.read_timed_out = true;
834 return 1;
835 }
836 if (!upload_complete
837 && now - progress.last_upload_progress > progress.write_timeout) {
838 progress.write_timed_out = true;
839 return 1;
840 }
841 return 0;
842 }

References arachne::pheidippides::anonymous_namespace{transport.cpp}::progress_context::downloaded, arachne::pheidippides::anonymous_namespace{transport.cpp}::progress_context::read_timed_out, arachne::pheidippides::anonymous_namespace{transport.cpp}::progress_context::upload_expected, arachne::pheidippides::anonymous_namespace{transport.cpp}::progress_context::uploaded, and arachne::pheidippides::anonymous_namespace{transport.cpp}::progress_context::write_timed_out.

Referenced by arachne::pheidippides::transport::execute().

Here is the caller graph for this function:

◆ trim_header_value()

std::string_view arachne::pheidippides::anonymous_namespace{transport.cpp}::trim_header_value ( std::string_view value)
nodiscardnoexcept

Definition at line 903 of file transport.cpp.

903 {
904 while (!value.empty()
905 && (value.back() == '\r' || value.back() == '\n')) {
906 value.remove_suffix(1U);
907 }
908 while (!value.empty()
909 && (value.front() == ' ' || value.front() == '\t')) {
910 value.remove_prefix(1U);
911 }
912 return value;
913 }

◆ valid_header()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::valid_header ( const http_header & header)
nodiscard

Definition at line 158 of file transport.cpp.

158 {
159 return valid_header_name(header.name)
160 && ascii_lower(header.name) != "host"
161 && std::ranges::none_of(header.value, [](const char character) {
162 return character == '\0' || character == '\r'
163 || character == '\n';
164 });
165 }

◆ valid_header_name()

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

Definition at line 138 of file transport.cpp.

138 {
139 if (name.empty()) {
140 return false;
141 }
142 constexpr std::string_view separators = "()<>@,;:\\\"/[]?={} \t";
143 return std::ranges::none_of(name, [&](const char character) {
144 return is_control(character)
145 || separators.find(character) != std::string_view::npos;
146 });
147 }

◆ valid_request_id()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::valid_request_id ( const std::string_view request_id)
nodiscard

Definition at line 117 of file transport.cpp.

117 {
118 if (request_id.empty() || request_id.size() > maximum_request_id_bytes
119 || std::isalnum(static_cast<unsigned char>(request_id.front()))
120 == 0) {
121 return false;
122 }
123 return std::ranges::all_of(request_id.substr(1), [](const char value) {
124 return std::isalnum(static_cast<unsigned char>(value)) != 0
125 || value == '.' || value == '_' || value == ':' || value == '-';
126 });
127 }

References maximum_request_id_bytes.

◆ validation_message()

std::string arachne::pheidippides::anonymous_namespace{transport.cpp}::validation_message ( const arachnespace::contracts::validation_result & validation)
nodiscard

Definition at line 1105 of file transport.cpp.

1107 {
1108 std::ostringstream message;
1109 message << "invalid fetch_request_v1 contract";
1110 for (const auto& diagnostic : validation.diagnostics) {
1111 message << "; "
1112 << (diagnostic.instance_path.empty()
1113 ? std::string_view { "/" }
1114 : std::string_view { diagnostic.instance_path })
1115 << " [" << diagnostic.code << "] " << diagnostic.message;
1116 }
1117 return message.str();
1118 }
std::vector< diagnostic > diagnostics
Definition contracts.hpp:38

◆ write_all()

bool arachne::pheidippides::anonymous_namespace{transport.cpp}::write_all ( const int descriptor,
const char * data,
std::size_t size,
std::string & error )
nodiscardnoexcept

Definition at line 844 of file transport.cpp.

847 {
848 while (size != 0) {
849 const ssize_t written = ::write(descriptor, data, size);
850 if (written < 0) {
851 if (errno == EINTR) {
852 continue;
853 }
854 error = std::string("artifact write failed: ")
855 + std::strerror(errno);
856 return false;
857 }
858 if (written == 0) {
859 error = "artifact write made no progress";
860 return false;
861 }
862 const auto amount = static_cast<std::size_t>(written);
863 data += amount;
864 size -= amount;
865 }
866 return true;
867 }

Referenced by body_callback().

Here is the caller graph for this function:

Variable Documentation

◆ curl_connection_share

CURLSH * arachne::pheidippides::anonymous_namespace{transport.cpp}::curl_connection_share = nullptr

Definition at line 46 of file transport.cpp.

Referenced by arachne::pheidippides::transport::execute(), and initialize_curl().

◆ curl_global_once

std::once_flag arachne::pheidippides::anonymous_namespace{transport.cpp}::curl_global_once

Definition at line 44 of file transport.cpp.

Referenced by initialize_curl().

◆ curl_share_mutex

std::recursive_mutex arachne::pheidippides::anonymous_namespace{transport.cpp}::curl_share_mutex

Definition at line 45 of file transport.cpp.

Referenced by curl_share_lock(), and curl_share_unlock().

◆ maximum_header_bytes

std::size_t arachne::pheidippides::anonymous_namespace{transport.cpp}::maximum_header_bytes = 1024U * 1024U
constexpr

Definition at line 37 of file transport.cpp.

Referenced by response_header_callback().

◆ maximum_redirects

std::size_t arachne::pheidippides::anonymous_namespace{transport.cpp}::maximum_redirects = 20U
constexpr

Definition at line 39 of file transport.cpp.

Referenced by arachne::pheidippides::transport::execute().

◆ maximum_request_id_bytes

std::size_t arachne::pheidippides::anonymous_namespace{transport.cpp}::maximum_request_id_bytes = 128U
constexpr

Definition at line 42 of file transport.cpp.

Referenced by artifact_id_for(), and valid_request_id().

◆ maximum_response_headers

std::size_t arachne::pheidippides::anonymous_namespace{transport.cpp}::maximum_response_headers = 1'024U
constexpr

Definition at line 38 of file transport.cpp.

Referenced by response_header_callback().

◆ maximum_transport_attempts

std::size_t arachne::pheidippides::anonymous_namespace{transport.cpp}::maximum_transport_attempts = 20U
constexpr

Definition at line 40 of file transport.cpp.

Referenced by arachne::pheidippides::transport::execute().

◆ maximum_url_bytes

std::size_t arachne::pheidippides::anonymous_namespace{transport.cpp}::maximum_url_bytes = 64U * 1024U
constexpr

Definition at line 41 of file transport.cpp.

◆ stage_sequence

std::atomic< std::uint64_t > arachne::pheidippides::anonymous_namespace{transport.cpp}::stage_sequence { 0 }