1588 {
1589 acquired_artifact_v1 result;
1590 result.artifact_id = "artifact-invalid";
1591 result.request_id = request.request_id;
1592 result.door_id = request.door_id;
1593 result.operation = request.operation;
1594 result.delivered_via
1598 result.source_url = request.url;
1599 result.started_at = std::chrono::system_clock::now();
1600
1601 if (request.contract != "fetch_request_v1"
1602 || request.format_version != 1U) {
1605 "unsupported fetch request format version"
1606 );
1607 }
1611 "request_id is not a supported stable identifier"
1612 );
1613 }
1615 if (request.timeout.count() <= 0 || request.connect_timeout.count() <= 0
1616 || request.read_timeout.count() <= 0
1617 || request.write_timeout.count() <= 0 || request.max_bytes == 0U) {
1620 "timeouts and max_bytes must be positive"
1621 );
1622 }
1623 if (request.initial_retry_delay.count() < 0
1624 || request.maximum_retry_delay.count() < 0
1625 || request.total_retry_delay_budget.count() < 0
1626 || request.initial_retry_delay > request.maximum_retry_delay) {
1629 "retry delays are negative or internally inconsistent"
1630 );
1631 }
1632 if (request.maximum_attempts == 0U
1633 || request.maximum_attempts > maximum_transport_attempts) {
1636 "maximum_attempts must be between one and twenty"
1637 );
1638 }
1639 if (request.redirects.maximum_redirects > maximum_redirects
1640 || (!request.redirects.follow
1641 && request.redirects.maximum_redirects != 0U)) {
1644 "redirect limit is inconsistent or exceeds the safety maximum"
1645 );
1646 }
1648 && (!request.body.empty() || request.body_artifact)) {
1651 "GET requests cannot contain a body"
1652 );
1653 }
1654 if (!request.body.empty() && request.body_artifact) {
1657 "request cannot contain both inline and artifact body bytes"
1658 );
1659 }
1661 != request.resume_artifact.has_value()) {
1664 "resume_download requires exactly one resume_artifact"
1665 );
1666 }
1667 if (request.resume_artifact
1669 || request.resume_artifact->byte_length == 0U
1670 || request.resume_artifact->byte_length >= request.max_bytes
1671 || request.resume_artifact->storage_ref
1672 == request.target_artifact_ref)) {
1675 "resume artifact, method, target, or size is inconsistent"
1676 );
1677 }
1678 if (request.body.size()
1679 > static_cast<std::size_t>(std::numeric_limits<curl_off_t>::max())) {
1682 "inline request body is too large for this transport build"
1683 );
1684 }
1685 if (!std::ranges::all_of(request.headers, valid_header)) {
1688 "request contains an invalid HTTP header"
1689 );
1690 }
1694 "target artifact reference is not a safe relative path"
1695 );
1696 }
1697
1698 std::string validation_error;
1699 auto current =
parse_url(request.url, validation_error);
1700 if (!current) {
1703 std::move(validation_error)
1704 );
1705 }
1707 *current, request.redirects.allowed_hosts, validation_error
1708 )) {
1711 std::move(validation_error)
1712 );
1713 }
1714
1715 std::optional<verified_body_file> body_file;
1716 if (request.body_artifact) {
1718 body_file = verified_body_file::open_and_verify(
1720 validation_error
1721 );
1722 if (!body_file) {
1724 std::move(result), body_status, std::move(validation_error)
1725 );
1726 }
1727 }
1728 std::optional<verified_body_file> resume_file;
1729 if (request.resume_artifact) {
1731 resume_file = verified_body_file::open_and_verify(
1733 validation_error
1734 );
1735 if (!resume_file) {
1737 std::move(result), resume_status, std::move(validation_error)
1738 );
1739 }
1740 }
1741
1744 );
1748 std::move(validation_error)
1749 );
1750 }
1751 std::error_code ec;
1752 const auto target_state = std::filesystem::symlink_status(target, ec);
1753 if ((!ec && std::filesystem::exists(target_state))
1754 || ec == std::errc::too_many_symbolic_link_levels) {
1757 "target artifact already exists"
1758 );
1759 }
1760 if (ec && ec != std::errc::no_such_file_or_directory) {
1763 "cannot inspect target artifact: " + ec.message()
1764 );
1765 }
1766
1767 std::optional<staging_file> staging;
1768 try {
1769 staging.emplace(target.parent_path());
1770 } catch (const std::exception& exception) {
1773 );
1774 }
1775
1777 if (!handle) {
1780 "curl_easy_init failed"
1781 );
1782 }
1783
1785 try {
1787 } catch (const std::exception& exception) {
1790 exception.what()
1791 );
1792 }
1793
1794 receive_context
received(staging->descriptor(), request.max_bytes);
1795 progress_context progress {
1796 .read_timeout = request.read_timeout,
1797 .write_timeout = request.write_timeout,
1799 && (!request.body.empty()
1800 || (body_file && body_file->length() != 0U)),
1801 };
1802 progress.reset();
1803 std::array<char, CURL_ERROR_SIZE> curl_error {};
1804 curl_easy_setopt(handle.get(), CURLOPT_ERRORBUFFER, curl_error.data());
1805 curl_easy_setopt(handle.get(), CURLOPT_NOSIGNAL, 1L);
1806 curl_easy_setopt(handle.get(), CURLOPT_SHARE, curl_connection_share);
1807 curl_easy_setopt(handle.get(), CURLOPT_MAXCONNECTS, 64L);
1808 curl_easy_setopt(handle.get(), CURLOPT_FOLLOWLOCATION, 0L);
1809 curl_easy_setopt(handle.get(), CURLOPT_NOPROGRESS, 0L);
1810 curl_easy_setopt(
1811 handle.get(), CURLOPT_XFERINFOFUNCTION, transfer_progress_callback
1812 );
1813 curl_easy_setopt(handle.get(), CURLOPT_XFERINFODATA, &progress);
1814 curl_easy_setopt(handle.get(), CURLOPT_PROTOCOLS_STR, "http,https");
1815 curl_easy_setopt(handle.get(), CURLOPT_REDIR_PROTOCOLS_STR, "http,https");
1816 curl_easy_setopt(handle.get(), CURLOPT_HTTP_CONTENT_DECODING, 0L);
1817 curl_easy_setopt(handle.get(), CURLOPT_FAILONERROR, 0L);
1818 curl_easy_setopt(
1819 handle.get(), CURLOPT_CONNECTTIMEOUT_MS,
1820 static_cast<long>(std::min<std::int64_t>(
1821 request.connect_timeout.count(), std::numeric_limits<long>::max()
1822 ))
1823 );
1824 curl_easy_setopt(handle.get(), CURLOPT_WRITEFUNCTION, body_callback);
1825 curl_easy_setopt(handle.get(), CURLOPT_WRITEDATA, &received);
1826 curl_easy_setopt(
1827 handle.get(), CURLOPT_HEADERFUNCTION, response_header_callback
1828 );
1829 curl_easy_setopt(handle.get(), CURLOPT_HEADERDATA, &received);
1830 curl_easy_setopt(
1831 handle.get(), CURLOPT_MAXFILESIZE_LARGE,
1832 static_cast<curl_off_t>(std::min<std::uint64_t>(
1833 request.max_bytes,
1834 static_cast<std::uint64_t>(std::numeric_limits<curl_off_t>::max())
1835 ))
1836 );
1837 if (request_headers) {
1838 curl_easy_setopt(
1839 handle.get(), CURLOPT_HTTPHEADER, request_headers.get()
1840 );
1841 }
1843 curl_easy_setopt(handle.get(), CURLOPT_POST, 1L);
1844 if (body_file) {
1845 curl_easy_setopt(
1846 handle.get(), CURLOPT_READFUNCTION, body_read_callback
1847 );
1848 curl_easy_setopt(handle.get(), CURLOPT_READDATA, &*body_file);
1849 curl_easy_setopt(
1850 handle.get(), CURLOPT_SEEKFUNCTION, body_seek_callback
1851 );
1852 curl_easy_setopt(handle.get(), CURLOPT_SEEKDATA, &*body_file);
1853 curl_easy_setopt(
1854 handle.get(), CURLOPT_POSTFIELDSIZE_LARGE,
1855 static_cast<curl_off_t>(body_file->length())
1856 );
1857 } else {
1858 curl_easy_setopt(
1859 handle.get(), CURLOPT_POSTFIELDS, request.body.data()
1860 );
1861 curl_easy_setopt(
1862 handle.get(), CURLOPT_POSTFIELDSIZE_LARGE,
1863 static_cast<curl_off_t>(request.body.size())
1864 );
1865 }
1866 } else {
1867 curl_easy_setopt(handle.get(), CURLOPT_HTTPGET, 1L);
1868 }
1869 if (resume_file) {
1870 curl_easy_setopt(
1871 handle.get(), CURLOPT_RESUME_FROM_LARGE,
1872 static_cast<curl_off_t>(resume_file->length())
1873 );
1874 }
1875
1876 const auto deadline = std::chrono::steady_clock::now() + request.timeout;
1877 const parsed_url initial_url = *current;
1878 bool transfer_completed = false;
1879 std::chrono::milliseconds retry_delay_spent { 0 };
1880 auto prepare_staging = [&]() -> bool {
1881 if (!staging->reset(validation_error)) {
1882 return false;
1883 }
1885 if (!resume_file) {
1886 return true;
1887 }
1888 if (!resume_file->rewind(validation_error)) {
1889 return false;
1890 }
1891 std::array<char, 64U * 1024U> buffer {};
1892 for (;;) {
1893 const std::size_t count
1894 = resume_file->read(buffer.data(), buffer.size());
1895 if (resume_file->read_failed()) {
1896 validation_error = resume_file->read_error();
1897 return false;
1898 }
1899 if (count == 0U) {
1900 break;
1901 }
1905 staging->descriptor(), buffer.data(), count,
1906 validation_error
1907 )) {
1908 if (validation_error.empty()) {
1909 validation_error = "resume artifact exceeds max_bytes";
1910 }
1911 return false;
1912 }
1914 std::as_bytes(std::span(buffer.data(), count))
1915 );
1916 received.byte_count +=
static_cast<std::uint64_t
>(count);
1917 }
1918 return true;
1919 };
1920 for (std::size_t attempt = 1;
1921 attempt <= request.maximum_attempts && !transfer_completed;
1922 ++attempt) {
1923 result.attempts = attempt;
1924 current = initial_url;
1925 result.redirect_chain.clear();
1926 std::size_t redirect_count = 0;
1927 if (!prepare_staging()) {
1930 std::move(validation_error)
1931 );
1932 }
1933
1934 for (;;) {
1936 if (remaining <= 0) {
1939 "transport deadline expired"
1940 );
1941 }
1942 curl_easy_setopt(handle.get(), CURLOPT_TIMEOUT_MS, remaining);
1943 if (body_file) {
1944 std::string rewind_error;
1945 if (!body_file->rewind(rewind_error)) {
1948 std::move(rewind_error)
1949 );
1950 }
1951 }
1952 curl_easy_setopt(
1953 handle.get(), CURLOPT_URL, current->normalized_url.c_str()
1954 );
1955 progress.reset();
1956 curl_error.fill('\0');
1957 const CURLcode curl_status = curl_easy_perform(handle.get());
1958
1959 char* effective_raw = nullptr;
1960 curl_easy_getinfo(
1961 handle.get(), CURLINFO_EFFECTIVE_URL, &effective_raw
1962 );
1963 result.effective_url = effective_raw == nullptr
1964 ? current->normalized_url
1965 : std::string(effective_raw);
1966 curl_easy_getinfo(
1967 handle.get(), CURLINFO_RESPONSE_CODE, &result.http_status
1968 );
1969 result.response_headers =
received.headers;
1970 result.byte_count =
received.byte_count;
1971
1972 if (
received.too_large || curl_status == CURLE_FILESIZE_EXCEEDED) {
1975 "response exceeded max_bytes"
1976 );
1977 }
1981 "response headers exceeded the safety limit"
1982 );
1983 }
1988 );
1989 }
1990 if (body_file && body_file->read_failed()) {
1993 body_file->read_error()
1994 );
1995 }
1996 if (curl_status != CURLE_OK) {
1998 = curl_status == CURLE_OPERATION_TIMEDOUT
1999 || progress.read_timed_out || progress.write_timed_out
2002 const std::string message = progress.read_timed_out
2003 ? "transport read-progress timeout expired"
2004 : progress.write_timed_out
2005 ? "transport write-progress timeout expired"
2006 : curl_error.front() == '\0'
2007 ? curl_easy_strerror(curl_status)
2008 : curl_error.data();
2010 && attempt < request.maximum_attempts
2012 if (may_retry) {
2013 const auto delay =
retry_delay(request, attempt, {});
2014 if (retry_delay_spent + delay
2015 > request.total_retry_delay_budget) {
2017 std::move(result),
2019 "network retry delay budget was exhausted"
2020 );
2021 }
2025 "transport deadline would expire during retry delay"
2026 );
2027 }
2028 retry_delay_spent += delay;
2029 std::this_thread::sleep_for(delay);
2030 break;
2031 }
2032 return fail_result(std::move(result), status, message);
2033 }
2034
2037 if (!request.redirects.follow) {
2040 "response attempted a redirect while redirects are "
2041 "disabled"
2042 );
2043 }
2044 if (redirect_count >= request.redirects.maximum_redirects) {
2046 std::move(result),
2048 "response exceeded the configured redirect limit"
2049 );
2050 }
2051
2052 std::string redirect_error;
2054 result.effective_url,
received.location, redirect_error
2055 );
2056 auto redirected = redirected_url
2057 ?
parse_url(*redirected_url, redirect_error)
2058 : std::nullopt;
2059 if (!redirected) {
2062 std::move(redirect_error)
2063 );
2064 }
2065 if (current->scheme == "https" && redirected->scheme == "http"
2066 && !request.redirects.allow_https_to_http) {
2069 "HTTPS-to-HTTP redirect is prohibited"
2070 );
2071 }
2073 *redirected, request.redirects.allowed_hosts,
2074 redirect_error
2075 )) {
2078 std::move(redirect_error)
2079 );
2080 }
2081 result.redirect_chain.emplace_back(redirected->normalized_url);
2082 current = std::move(redirected);
2083 ++redirect_count;
2084 if (!prepare_staging()) {
2087 std::move(validation_error)
2088 );
2089 }
2090 continue;
2091 }
2092 if (resume_file && result.http_status != 206) {
2095 "provider did not honor the resumable byte-range request"
2096 );
2097 }
2100 && attempt < request.maximum_attempts) {
2101 const auto delay
2103 if (retry_delay_spent + delay
2104 > request.total_retry_delay_budget) {
2106 std::move(result),
2108 "HTTP retry delay budget was exhausted"
2109 );
2110 }
2114 "transport deadline would expire during retry delay"
2115 );
2116 }
2117 retry_delay_spent += delay;
2118 std::this_thread::sleep_for(delay);
2119 break;
2120 }
2121 transfer_completed = true;
2122 break;
2123 }
2124 }
2125
2126 std::string synchronize_error;
2127 if (!staging->synchronize(synchronize_error)) {
2130 std::move(synchronize_error)
2131 );
2132 }
2133 const std::string digest =
received.hasher.finish_hex();
2134 if (request.expected_sha256 && digest != *request.expected_sha256) {
2137 "delivered bytes do not match expected SHA-256"
2138 );
2139 }
2140 staging->close();
2141
2142 ec.clear();
2143 std::filesystem::create_hard_link(staging->path(), target, ec);
2144 if (ec) {
2149 std::move(result), status,
2150 "cannot atomically publish artifact: " + ec.message()
2151 );
2152 }
2153 ec.clear();
2154 std::filesystem::remove(staging->path(), ec);
2155 if (!ec) {
2156 staging->forget();
2157 }
2158
2160 result.artifact_ref = request.target_artifact_ref;
2161 result.sha256 = digest;
2162 result.byte_count =
received.byte_count;
2163 result.error_message.clear();
2164 result.completed_at = std::chrono::system_clock::now();
2165 return result;
2166}
bool is_safe_relative_artifact_ref(std::string_view artifact_ref) noexcept
std::filesystem::path safe_artifact_path(const std::filesystem::path &root, std::string_view artifact_ref)
acquired_artifact_v1 fail_result(acquired_artifact_v1 result, const transport_status status, std::string message)
bool is_redirect_status(const long status) noexcept
header_list build_request_headers(const std::vector< http_header > &headers)
std::string artifact_id_for(const std::string_view request_id)
bool valid_request_id(const std::string_view request_id)
std::unique_ptr< curl_slist, curl_slist_deleter > header_list
bool is_retryable_curl_status(const CURLcode status) noexcept
bool write_all(const int descriptor, const char *data, std::size_t size, std::string &error) noexcept
std::optional< std::chrono::milliseconds > retry_after_delay(const std::vector< http_header > &headers)
std::optional< std::string > resolve_redirect(const std::string &base, const std::string &location, std::string &error)
std::chrono::milliseconds retry_delay(const fetch_request_v1 &request, const std::size_t attempt, const std::vector< http_header > &headers)
bool is_retryable_status(const long status) noexcept
bool ensure_safe_parent(const std::filesystem::path &root, const std::filesystem::path &target, std::string &error)
std::unique_ptr< CURL, curl_easy_deleter > easy_handle
std::optional< parsed_url > parse_url(const std::string &value, std::string &error)
long timeout_milliseconds(const std::chrono::steady_clock::time_point deadline) noexcept
bool host_allowed(const parsed_url &url, const std::vector< std::string > &allowed_hosts, std::string &error)
@ redirect_limit_exceeded