3#include <boost/asio/ip/tcp.hpp>
4#include <boost/asio/redirect_error.hpp>
5#include <boost/asio/ssl.hpp>
6#include <boost/asio/use_awaitable.hpp>
7#include <boost/beast/core.hpp>
8#include <boost/beast/http.hpp>
9#include <boost/beast/ssl.hpp>
10#include <boost/url/parse.hpp>
11#include <boost/url/url.hpp>
17namespace asio = boost::asio;
18namespace beast = boost::beast;
19namespace http = beast::http;
20namespace urls = boost::urls;
21using tcp = asio::ip::tcp;
25constexpr auto httpTimeout = std::chrono::seconds(15);
26constexpr int maxRedirects = 5;
28std::string ensureSlash(std::string value) {
29 if (value.empty() || value.front() !=
'/') value.insert(value.begin(),
'/');
33std::string joinUrlPath(std::string_view prefix, std::string_view path) {
34 if (prefix.empty() || prefix ==
"/")
return ensureSlash(std::string(path));
35 if (path.empty())
return ensureSlash(std::string(prefix));
37 std::string out(prefix);
38 if (!out.ends_with(
'/')) out.push_back(
'/');
39 if (path.starts_with(
'/')) path.remove_prefix(1);
41 return ensureSlash(std::move(out));
44void requireHttpScheme(
const urls::url_view_base& url) {
45 if (url.scheme() !=
"http" && url.scheme() !=
"https") {
50std::string servicePort(
const urls::url_view_base& url) {
51 if (url.has_port())
return std::string(url.port());
52 return url.scheme() ==
"https" ?
"443" :
"80";
55std::string requestTarget(
const urls::url_view_base& url) {
56 auto target = std::string(url.encoded_path());
57 if (target.empty()) target =
"/";
58 if (url.has_query()) {
59 target.push_back(
'?');
60 target += std::string(url.encoded_query());
65urls::url parseEndpoint(std::string endpoint, std::string_view path) {
66 const auto parsed = urls::parse_uri(endpoint);
68 std::cerr <<
"invalid bucket endpoint: " << endpoint <<
"\n";
72 urls::url url(*parsed);
74 if (url.scheme() ==
"s3") url.set_scheme(
"https");
75 requireHttpScheme(url);
76 url.set_path(joinUrlPath(url.path(), path));
90 if (!bucket.
scheme.empty()) {
91 const auto scheme = bucket.
scheme ==
"s3" ? std::string(
"https") : bucket.
scheme;
95 return parseEndpoint(scheme +
"://" + std::string(path), {});
98 const auto slash = path.find(
'/');
99 if (slash == std::string_view::npos) {
103 const auto scheme = path.substr(0, slash);
104 if (scheme !=
"http" && scheme !=
"https") {
107 return parseEndpoint(std::string(scheme) +
"://" + std::string(path.substr(slash + 1)), {});
111asio::ssl::context& tlsContext() {
112 static asio::ssl::context context = [] {
113 asio::ssl::context created(asio::ssl::context::tls_client);
114 created.set_default_verify_paths();
115 created.set_verify_mode(asio::ssl::verify_peer);
121template <
typename Stream>
122asio::awaitable<http::response<http::vector_body<uint8_t>>> exchange(Stream& stream, std::string_view hostHeader, std::string_view target,
124 http::request<http::empty_body> req{http::verb::get, target, 11};
125 req.set(http::field::host, hostHeader);
126 req.set(http::field::user_agent,
"haio-cdn");
127 for (
const auto& [name, value] : headers) req.set(name, value);
129 co_await http::async_write(stream, req, asio::use_awaitable);
131 beast::flat_buffer buffer;
132 http::response<http::vector_body<uint8_t>> res;
133 co_await http::async_read(stream, buffer, res, asio::use_awaitable);
137asio::awaitable<http::response<http::vector_body<uint8_t>>> request(
const urls::url& url,
const Haio::Cdn::Bucket::Headers& headers) {
138 auto executor =
co_await asio::this_coro::executor;
139 tcp::resolver resolver(executor);
141 const auto host = std::string(url.host());
142 const auto results =
co_await resolver.async_resolve(host, servicePort(url), asio::use_awaitable);
143 const auto hostHeader = std::string(url.encoded_host_and_port());
144 const auto target = requestTarget(url);
146 http::response<http::vector_body<uint8_t>> res;
147 beast::error_code ec;
149 if (url.scheme() ==
"https") {
150 beast::ssl_stream<beast::tcp_stream> stream(executor, tlsContext());
151 if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) {
154 std::cerr <<
"could not set sni for " << host <<
"\n";
157 stream.set_verify_callback(asio::ssl::host_name_verification(host));
159 beast::get_lowest_layer(stream).expires_after(httpTimeout);
160 co_await beast::get_lowest_layer(stream).async_connect(results, asio::use_awaitable);
161 co_await stream.async_handshake(asio::ssl::stream_base::client, asio::use_awaitable);
163 res =
co_await exchange(stream, hostHeader, target, headers);
166 co_await stream.async_shutdown(asio::redirect_error(asio::use_awaitable, ec));
168 beast::tcp_stream stream(executor);
169 stream.expires_after(httpTimeout);
170 co_await stream.async_connect(results, asio::use_awaitable);
172 res =
co_await exchange(stream, hostHeader, target, headers);
174 stream.socket().shutdown(tcp::socket::shutdown_both, ec);
181urls::url redirectTarget(
const urls::url& from, std::string_view location) {
182 auto ref = urls::parse_uri_reference(location);
184 std::cerr <<
"upstream sent an invalid redirect: " << location <<
"\n";
189 if (
const auto resolved = urls::resolve(from, *ref, next); !resolved) {
190 std::cerr <<
"could not resolve redirect: " << location <<
"\n";
194 requireHttpScheme(next);
199 for (
int hop = 0;; hop++) {
200 auto res =
co_await request(url, headers);
201 const auto status = res.result_int();
203 if (status >= 300 && status < 400) {
204 if (hop >= maxRedirects) {
207 const auto location = res[http::field::location];
208 if (location.empty()) {
215 url = redirectTarget(url, std::string_view(location.data(), location.size()));
219 if (status < 200 || status >= 300) {
223 const auto contentType = res[http::field::content_type];
228 auto format = found.format;
236 co_return Haio::Blob{format, found.color, std::string(contentType), std::move(pathForFormat), std::move(res.body())};
245 const auto parsed = urls::parse_uri(url);
247 std::cerr <<
"invalid signed url: " << url <<
"\n";
250 co_return co_await fetchUrl(urls::url{*parsed}, std::move(pathForFormat), std::move(headers));
255 auto url = openBucketTarget(bucket, path);
256 auto forFormat = requestTarget(url);
257 co_return co_await fetchUrl(std::move(url), std::move(forFormat));
259 co_return co_await fetchUrl(parseEndpoint(bucket.
url, path), std::move(path));
std::map< std::string, std::string > Headers
boost::asio::awaitable< Blob > fetchUrlWith(std::string url, std::string pathForFormat, Headers headers)
boost::asio::awaitable< Blob > fetchHttp(const BucketConfig &bucket, std::string path)
Format formatFromContentType(std::string_view contentType)
Format formatFromExtension(std::string_view path)
Failure(Haio::ErrorCode why, const std::string &what)