#include <dds/dym.hpp> | #include <dds/dym.hpp> | ||||
#include <dds/error/errors.hpp> | #include <dds/error/errors.hpp> | ||||
#include <dds/http/session.hpp> | |||||
#include <dds/pkg/db.hpp> | #include <dds/pkg/db.hpp> | ||||
#include <dds/pkg/get/get.hpp> | #include <dds/pkg/get/get.hpp> | ||||
#include <dds/util/http/pool.hpp> | |||||
#include <dds/util/result.hpp> | #include <dds/util/result.hpp> | ||||
#include <boost/leaf/handle_exception.hpp> | #include <boost/leaf/handle_exception.hpp> | ||||
#include <json5/parse_data.hpp> | #include <json5/parse_data.hpp> | ||||
#include <neo/url.hpp> | |||||
namespace dds::cli::cmd { | namespace dds::cli::cmd { | ||||
url_err.what()); | url_err.what()); | ||||
return 1; | return 1; | ||||
}, | }, | ||||
[&](const json5::parse_error& e, dds::e_http_url bad_url) { | |||||
[&](const json5::parse_error& e, neo::url bad_url) { | |||||
dds_log(error, | dds_log(error, | ||||
"Error parsing JSON5 document package downloaded from [{}]: {}", | "Error parsing JSON5 document package downloaded from [{}]: {}", | ||||
bad_url.value, | |||||
bad_url.to_string(), | |||||
e.what()); | e.what()); | ||||
return 1; | return 1; | ||||
}, | }, | ||||
dds_log(error, "Error accessing the package database: {}", e.message); | dds_log(error, "Error accessing the package database: {}", e.message); | ||||
return 1; | return 1; | ||||
}, | }, | ||||
[&](dds::e_system_error_exc e, dds::e_http_connect conn) { | |||||
[&](dds::e_system_error_exc e, dds::network_origin conn) { | |||||
dds_log(error, | dds_log(error, | ||||
"Error opening connection to [{}:{}]: {}", | "Error opening connection to [{}:{}]: {}", | ||||
conn.host, | |||||
conn.hostname, | |||||
conn.port, | conn.port, | ||||
e.message); | e.message); | ||||
return 1; | return 1; |
#include "../options.hpp" | #include "../options.hpp" | ||||
#include <dds/http/session.hpp> | |||||
#include <dds/pkg/cache.hpp> | #include <dds/pkg/cache.hpp> | ||||
#include <dds/sdist/dist.hpp> | #include <dds/sdist/dist.hpp> | ||||
#include <dds/util/result.hpp> | #include <dds/util/result.hpp> |
#include "./pkg_repo_err_handle.hpp" | #include "./pkg_repo_err_handle.hpp" | ||||
#include <dds/http/session.hpp> | |||||
#include <dds/util/http/pool.hpp> | |||||
#include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
#include <dds/util/result.hpp> | #include <dds/util/result.hpp> | ||||
#include <boost/leaf/handle_exception.hpp> | #include <boost/leaf/handle_exception.hpp> | ||||
#include <json5/parse_data.hpp> | #include <json5/parse_data.hpp> | ||||
#include <neo/url/parse.hpp> | |||||
#include <neo/url.hpp> | |||||
int dds::cli::cmd::handle_pkg_repo_remote_errors(std::function<int()> fn) { | int dds::cli::cmd::handle_pkg_repo_remote_errors(std::function<int()> fn) { | ||||
return boost::leaf::try_catch( | return boost::leaf::try_catch( | ||||
dds::capture_exception(); | dds::capture_exception(); | ||||
} | } | ||||
}, | }, | ||||
[&](neo::url_validation_error url_err, dds::e_url_string bad_url) { | |||||
dds_log(error, "Invalid URL [{}]: {}", bad_url.value, url_err.what()); | |||||
[](neo::url_validation_error url_err, neo::url bad_url) { | |||||
dds_log(error, "Invalid URL [{}]: {}", bad_url.to_string(), url_err.what()); | |||||
return 1; | return 1; | ||||
}, | }, | ||||
[&](const json5::parse_error& e, dds::e_http_url bad_url) { | |||||
[](dds::http_status_error err, dds::http_response_info resp, neo::url bad_url) { | |||||
dds_log(error, | |||||
"An HTTP error occured while requesting [{}]: HTTP Status {} {}", | |||||
err.what(), | |||||
bad_url.to_string(), | |||||
resp.status, | |||||
resp.status_message); | |||||
return 1; | |||||
}, | |||||
[](const json5::parse_error& e, neo::url bad_url) { | |||||
dds_log(error, | dds_log(error, | ||||
"Error parsing JSON downloaded from URL [{}]: {}", | "Error parsing JSON downloaded from URL [{}]: {}", | ||||
bad_url.value, | |||||
bad_url.to_string(), | |||||
e.what()); | e.what()); | ||||
return 1; | return 1; | ||||
}, | }, | ||||
[](dds::e_sqlite3_error_exc e, dds::e_url_string url) { | |||||
dds_log(error, "Error accessing remote database (From {}): {}", url.value, e.message); | |||||
[](dds::e_sqlite3_error_exc e, neo::url url) { | |||||
dds_log(error, "Error accessing remote database [{}]: {}", url.to_string(), e.message); | |||||
return 1; | return 1; | ||||
}, | }, | ||||
[](dds::e_sqlite3_error_exc e) { | [](dds::e_sqlite3_error_exc e) { | ||||
dds_log(error, "Unexpected database error: {}", e.message); | dds_log(error, "Unexpected database error: {}", e.message); | ||||
return 1; | return 1; | ||||
}, | }, | ||||
[&](dds::e_system_error_exc e, dds::e_http_connect conn) { | |||||
[](dds::e_system_error_exc e, dds::network_origin conn) { | |||||
dds_log(error, | dds_log(error, | ||||
"Error opening connection to [{}:{}]: {}", | |||||
conn.host, | |||||
"Error communicating with [{}://{}:{}]: {}", | |||||
conn.protocol, | |||||
conn.hostname, | |||||
conn.port, | conn.port, | ||||
e.message); | e.message); | ||||
return 1; | return 1; |
#include "./session.hpp" | |||||
#include <dds/error/errors.hpp> | |||||
#include <dds/util/fs.hpp> | |||||
#include <dds/util/log.hpp> | |||||
#include <dds/util/result.hpp> | |||||
#include <fmt/format.h> | |||||
#include <fmt/ostream.h> | |||||
#include <neo/event.hpp> | |||||
#include <neo/http/parse/chunked.hpp> | |||||
#include <neo/http/request.hpp> | |||||
#include <neo/http/response.hpp> | |||||
#include <neo/io/openssl/init.hpp> | |||||
#include <neo/io/stream/buffers.hpp> | |||||
#include <neo/io/stream/file.hpp> | |||||
#include <neo/string_io.hpp> | |||||
using namespace dds; | |||||
namespace { | |||||
struct simple_request { | |||||
std::string method; | |||||
std::vector<std::pair<std::string, std::string>> headers; | |||||
}; | |||||
template <typename Out, typename In> | |||||
void download_into(Out&& out, In&& in, http_response_info resp) { | |||||
auto resp_te = resp.headers.find(neo::http::standard_headers::transfer_encoding); | |||||
if (resp_te) { | |||||
if (resp_te->value != "chunked") { | |||||
throw std::runtime_error(fmt::format( | |||||
"We can't yet handle HTTP responses that set Transfer-Encoding [Transfer " | |||||
"encoding is '{}']", | |||||
resp_te->value)); | |||||
} | |||||
neo::http::chunked_buffers chunked_in{in}; | |||||
buffer_copy(out, chunked_in); | |||||
} else { | |||||
auto clen = resp.headers.find(neo::http::standard_headers::content_length); | |||||
neo_assert(invariant, !!clen, "HTTP response has no Content-Length header??"); | |||||
buffer_copy(out, in, std::stoull(clen->value)); | |||||
} | |||||
} | |||||
} // namespace | |||||
http_session http_session::connect_for(const neo::url& url) { | |||||
if (!url.host) { | |||||
throw_user_error< | |||||
errc::invalid_remote_url>("URL is invalid for network connection [{}]: No host segment", | |||||
url.to_string()); | |||||
} | |||||
auto sub = neo::subscribe( | |||||
[&](neo::address::ev_resolve ev) { | |||||
dds_log(trace, "Resolving '{}:{}'", ev.host, ev.service); | |||||
neo::bubble_event(ev); | |||||
}, | |||||
[&](neo::socket::ev_connect ev) { | |||||
dds_log(trace, "Connecting {}", *url.host); | |||||
neo::bubble_event(ev); | |||||
}, | |||||
[&](neo::ssl::ev_handshake ev) { | |||||
dds_log(trace, "TLS handshake..."); | |||||
neo::bubble_event(ev); | |||||
}); | |||||
if (url.scheme == "http") { | |||||
return connect(*url.host, url.port_or_default_port_or(80)); | |||||
} else if (url.scheme == "https") { | |||||
return connect_ssl(*url.host, url.port_or_default_port_or(443)); | |||||
} else { | |||||
throw_user_error<errc::invalid_remote_url>("URL is invalid [{}]", url.to_string()); | |||||
} | |||||
} | |||||
http_session http_session::connect(const std::string& host, int port) { | |||||
DDS_E_SCOPE(e_http_connect{host, port}); | |||||
auto addr = neo::address::resolve(host, std::to_string(port)); | |||||
auto sock = neo::socket::open_connected(addr, neo::socket::type::stream); | |||||
return http_session{std::move(sock), fmt::format("{}:{}", host, port)}; | |||||
} | |||||
http_session http_session::connect_ssl(const std::string& host, int port) { | |||||
DDS_E_SCOPE(e_http_connect{host, port}); | |||||
auto addr = neo::address::resolve(host, std::to_string(port)); | |||||
auto sock = neo::socket::open_connected(addr, neo::socket::type::stream); | |||||
static neo::ssl::openssl_app_init ssl_init; | |||||
static neo::ssl::context ssl_ctx{neo::ssl::protocol::tls_any, neo::ssl::role::client}; | |||||
neo::stream_io_buffers sock_in{sock}; | |||||
ssl_engine ssl_eng{ssl_ctx, sock_in, neo::stream_io_buffers{sock}}; | |||||
ssl_eng.connect(); | |||||
return http_session(std::move(sock), fmt::format("{}:{}", host, port), std::move(ssl_eng)); | |||||
} | |||||
void http_session::send_head(http_request_params params) { | |||||
neo_assert_always(invariant, | |||||
_state == _state_t::ready, | |||||
"Invalid state for HTTP session to send a request head", | |||||
_state, | |||||
params.method, | |||||
params.path, | |||||
params.query); | |||||
neo::emit(ev_http_request{params}); | |||||
neo::http::request_line start_line{ | |||||
.method_view = params.method, | |||||
.target = neo::http::origin_form_target{ | |||||
.path_view = params.path, | |||||
.query_view = params.query, | |||||
.has_query = !params.query.empty(), | |||||
.parse_tail = neo::const_buffer(), | |||||
}, | |||||
.http_version = neo::http::version::v1_1, | |||||
.parse_tail = neo::const_buffer(), | |||||
}; | |||||
dds_log(trace, "Send: HTTP {} to {}{}", params.method, host_string(), params.path); | |||||
auto cl_str = std::to_string(params.content_length); | |||||
// TODO: GZip downloads | |||||
std::pair<std::string_view, std::string_view> headers[] = { | |||||
{"Host", host_string()}, | |||||
{"Accept", "*/*"}, | |||||
{"Content-Length", cl_str}, | |||||
}; | |||||
_do_io( | |||||
[&](auto&& io) { neo::http::write_request(io, start_line, headers, neo::const_buffer()); }); | |||||
_state = _state_t::sent_request; | |||||
} | |||||
http_response_info http_session::recv_head() { | |||||
neo_assert_always(invariant, | |||||
_state == _state_t::sent_request, | |||||
"Invalid state to receive response head", | |||||
_state); | |||||
auto r | |||||
= _do_io([&](auto&& io) { return neo::http::read_response_head<http_response_info>(io); }); | |||||
dds_log(trace, "Recv: HTTP {} {}", r.status, r.status_message); | |||||
_state = _state_t::recvd_head; | |||||
neo::emit(ev_http_response_begin{r}); | |||||
return r; | |||||
} | |||||
std::string http_session::request(http_request_params params) { | |||||
send_head(params); | |||||
auto resp_head = recv_head(); | |||||
neo::string_dynbuf_io resp_body; | |||||
_do_io([&](auto&& io) { download_into(resp_body, io, resp_head); }); | |||||
neo::emit(ev_http_response_end{resp_head}); | |||||
auto body_size = resp_body.available(); | |||||
auto str = std::move(resp_body.string()); | |||||
str.resize(body_size); | |||||
_state = _state_t::ready; | |||||
return str; | |||||
} | |||||
void http_session::recv_body_to_file(http_response_info const& resp_head, | |||||
const std::filesystem::path& dest) { | |||||
neo_assert_always(invariant, | |||||
_state == _state_t::recvd_head, | |||||
"Invalid state to receive request body", | |||||
_state, | |||||
dest); | |||||
auto ofile = neo::file_stream::open(dest, neo::open_mode::write | neo::open_mode::create); | |||||
neo::stream_io_buffers file_out{ofile}; | |||||
_do_io([&](auto&& io) { download_into(file_out, io, resp_head); }); | |||||
neo::emit(ev_http_response_end{resp_head}); | |||||
_state = _state_t::ready; | |||||
} | |||||
void http_session::download_file(http_request_params params, const std::filesystem::path& dest) { | |||||
send_head(params); | |||||
auto resp_head = recv_head(); | |||||
if (resp_head.is_error()) { | |||||
throw_external_error< | |||||
errc::http_download_failure>("Failed to download file from {}{} to {}: HTTP {} {}", | |||||
host_string(), | |||||
params.path, | |||||
dest, | |||||
resp_head.status, | |||||
resp_head.status_message); | |||||
} | |||||
if (resp_head.is_redirect()) { | |||||
throw_external_error<errc::http_download_failure>( | |||||
"dds does not yet support HTTP redirects when downloading data. An HTTP redirect " | |||||
"was encountered when accessing {}{}: It wants to redirect to {}", | |||||
host_string(), | |||||
params.path, | |||||
resp_head.headers["Location"].value); | |||||
} | |||||
recv_body_to_file(resp_head, dest); | |||||
} |
#pragma once | |||||
#include <neo/http/headers.hpp> | |||||
#include <neo/http/version.hpp> | |||||
#include <neo/io/openssl/engine.hpp> | |||||
#include <neo/io/stream/buffers.hpp> | |||||
#include <neo/io/stream/socket.hpp> | |||||
#include <neo/string_io.hpp> | |||||
#include <neo/url.hpp> | |||||
#include <filesystem> | |||||
#include <string> | |||||
namespace dds { | |||||
struct e_http_url { | |||||
std::string value; | |||||
}; | |||||
struct e_http_connect { | |||||
std::string host; | |||||
int port; | |||||
}; | |||||
struct http_request_params { | |||||
std::string_view method; | |||||
std::string_view path; | |||||
std::string_view query = ""; | |||||
std::size_t content_length = 0; | |||||
}; | |||||
struct ev_http_request { | |||||
const http_request_params& request; | |||||
}; | |||||
struct http_response_info { | |||||
int status; | |||||
std::string status_message; | |||||
neo::http::version version; | |||||
neo::http::headers headers; | |||||
std::size_t head_byte_size = 0; | |||||
void throw_for_status() const; | |||||
bool is_client_error() const noexcept { return status >= 400 && status < 500; } | |||||
bool is_server_error() const noexcept { return status >= 500 && status < 600; } | |||||
bool is_error() const noexcept { return is_client_error() || is_server_error(); } | |||||
bool is_redirect() const noexcept { return status >= 300 && status < 400; } | |||||
}; | |||||
struct ev_http_response_begin { | |||||
const http_response_info& response; | |||||
}; | |||||
struct ev_http_response_end { | |||||
const http_response_info& response; | |||||
}; | |||||
enum class http_kind { | |||||
plain, | |||||
ssl, | |||||
}; | |||||
class http_session { | |||||
neo::socket _conn; | |||||
std::string _host_string; | |||||
using sock_buffers = neo::stream_io_buffers<neo::socket&>; | |||||
sock_buffers _sock_in{_conn}; | |||||
using ssl_engine = neo::ssl::engine<sock_buffers&, sock_buffers>; | |||||
using ssl_buffers = neo::stream_io_buffers<ssl_engine>; | |||||
std::optional<ssl_buffers> _ssl_in; | |||||
enum _state_t { ready, sent_request, recvd_head } _state = ready; | |||||
template <typename F> | |||||
decltype(auto) _do_io(F&& fn) { | |||||
if (_ssl_in) { | |||||
return fn(*_ssl_in); | |||||
} else { | |||||
return fn(_sock_in); | |||||
} | |||||
} | |||||
void _rebind_refs() { | |||||
if (_ssl_in) { | |||||
_ssl_in->stream().rebind_input(_sock_in); | |||||
_ssl_in->stream().output().rebind_stream(_conn); | |||||
} | |||||
} | |||||
public: | |||||
explicit http_session(neo::socket s, std::string host_header) | |||||
: _conn(std::move(s)) | |||||
, _host_string(std::move(host_header)) {} | |||||
explicit http_session(neo::socket s, std::string host_header, ssl_engine&& eng) | |||||
: _conn(std::move(s)) | |||||
, _host_string(std::move(host_header)) | |||||
, _sock_in(_conn, std::move(eng.input().io_buffers())) | |||||
, _ssl_in(std::move(eng)) { | |||||
_rebind_refs(); | |||||
} | |||||
http_session(http_session&& other) noexcept | |||||
: _conn(std::move(other._conn)) | |||||
, _host_string(std::move(other._host_string)) | |||||
, _sock_in(_conn, std::move(other._sock_in.io_buffers())) | |||||
, _ssl_in(std::move(other._ssl_in)) { | |||||
_rebind_refs(); | |||||
} | |||||
http_session& operator=(http_session&& other) noexcept { | |||||
_conn = std::move(other._conn); | |||||
_host_string = std::move(other._host_string); | |||||
_sock_in.io_buffers() = std::move(other._sock_in.io_buffers()); | |||||
_ssl_in = std::move(other._ssl_in); | |||||
_rebind_refs(); | |||||
return *this; | |||||
} | |||||
void send_head(http_request_params); | |||||
http_response_info recv_head(); | |||||
void recv_body_to_file(http_response_info const& res_head, const std::filesystem::path& dest); | |||||
std::string_view host_string() const noexcept { return _host_string; } | |||||
static http_session connect(const std::string& host, int port); | |||||
static http_session connect_ssl(const std::string& host, int port); | |||||
static http_session connect_for(const neo::url& url); | |||||
std::string request(http_request_params); | |||||
std::string request_get(std::string_view path) { | |||||
return request({.method = "GET", .path = path}); | |||||
} | |||||
void download_file(http_request_params, const std::filesystem::path& dest); | |||||
}; | |||||
} // namespace dds |
#include <dds/http/session.hpp> | |||||
#include <catch2/catch.hpp> | |||||
TEST_CASE("Create an HTTP session") { | |||||
auto sess = dds::http_session::connect("google.com", 80); | |||||
auto resp = sess.request_get("/"); | |||||
} |
#include "./http.hpp" | #include "./http.hpp" | ||||
#include <dds/error/errors.hpp> | #include <dds/error/errors.hpp> | ||||
#include <dds/http/session.hpp> | |||||
#include <dds/temp.hpp> | #include <dds/temp.hpp> | ||||
#include <dds/util/http/pool.hpp> | |||||
#include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
#include <neo/io/stream/buffers.hpp> | |||||
#include <neo/io/stream/file.hpp> | |||||
#include <neo/tar/util.hpp> | #include <neo/tar/util.hpp> | ||||
#include <neo/url.hpp> | #include <neo/url.hpp> | ||||
#include <neo/url/query.hpp> | #include <neo/url/query.hpp> | ||||
using namespace dds; | using namespace dds; | ||||
namespace { | |||||
void http_download_with_redir(neo::url url, path_ref dest) { | |||||
for (auto redir_count = 0;; ++redir_count) { | |||||
auto sess = http_session::connect_for(url); | |||||
sess.send_head({.method = "GET", .path = url.path}); | |||||
auto res_head = sess.recv_head(); | |||||
if (res_head.is_error()) { | |||||
dds_log(error, | |||||
"Received an HTTP {} {} for [{}]", | |||||
res_head.status, | |||||
res_head.status_message, | |||||
url.to_string()); | |||||
throw_external_error<errc::http_download_failure>( | |||||
"HTTP error while downloading resource [{}]. Got: HTTP {} '{}'", | |||||
url.to_string(), | |||||
res_head.status, | |||||
res_head.status_message); | |||||
} | |||||
if (res_head.is_redirect()) { | |||||
dds_log(trace, | |||||
"Received HTTP redirect for [{}]: {} {}", | |||||
url.to_string(), | |||||
res_head.status, | |||||
res_head.status_message); | |||||
if (redir_count == 100) { | |||||
throw_external_error<errc::http_download_failure>("Too many redirects on URL"); | |||||
} | |||||
auto loc = res_head.headers.find("Location"); | |||||
if (!loc) { | |||||
throw_external_error<errc::http_download_failure>( | |||||
"HTTP endpoint told us to redirect without sending a 'Location' header " | |||||
"(Received " | |||||
"HTTP {} '{}')", | |||||
res_head.status, | |||||
res_head.status_message); | |||||
} | |||||
dds_log(debug, | |||||
"Redirect [{}]: {} {} to [{}]", | |||||
url.to_string(), | |||||
res_head.status, | |||||
res_head.status_message, | |||||
loc->value); | |||||
auto new_url = neo::url::try_parse(loc->value); | |||||
auto err = std::get_if<neo::url_parse_error>(&new_url); | |||||
if (err) { | |||||
throw_external_error<errc::http_download_failure>( | |||||
"Server returned an invalid URL for HTTP redirection [{}]", loc->value); | |||||
} | |||||
url = std::move(std::get<neo::url>(new_url)); | |||||
continue; | |||||
} | |||||
// Not a redirect nor an error: Download the body | |||||
dds_log(trace, | |||||
"HTTP {} {} [{}]: Saving to [{}]", | |||||
res_head.status, | |||||
res_head.status_message, | |||||
url.to_string(), | |||||
dest.string()); | |||||
sess.recv_body_to_file(res_head, dest); | |||||
break; | |||||
} | |||||
} | |||||
} // namespace | |||||
void http_remote_listing::pull_source(path_ref dest) const { | void http_remote_listing::pull_source(path_ref dest) const { | ||||
neo::url url; | neo::url url; | ||||
try { | try { | ||||
auto dl_path = tdir.path() / fname; | auto dl_path = tdir.path() / fname; | ||||
fs::create_directory(dl_path.parent_path()); | fs::create_directory(dl_path.parent_path()); | ||||
http_download_with_redir(url, dl_path); | |||||
http_pool pool; | |||||
auto [client, resp] = pool.request_with_redirects("GET", url); | |||||
auto dl_file = neo::file_stream::open(dl_path, neo::open_mode::write); | |||||
client.recv_body_into(resp, neo::stream_io_buffers{dl_file}); | |||||
neo_assert(invariant, | neo_assert(invariant, | ||||
fs::is_regular_file(dl_path), | fs::is_regular_file(dl_path), |
#include "./remote.hpp" | #include "./remote.hpp" | ||||
#include <dds/error/errors.hpp> | #include <dds/error/errors.hpp> | ||||
#include <dds/http/session.hpp> | |||||
#include <dds/temp.hpp> | #include <dds/temp.hpp> | ||||
#include <dds/util/http/pool.hpp> | |||||
#include <dds/util/log.hpp> | #include <dds/util/log.hpp> | ||||
#include <dds/util/result.hpp> | #include <dds/util/result.hpp> | ||||
#include <neo/event.hpp> | #include <neo/event.hpp> | ||||
#include <neo/io/stream/buffers.hpp> | |||||
#include <neo/io/stream/file.hpp> | |||||
#include <neo/scope.hpp> | #include <neo/scope.hpp> | ||||
#include <neo/sqlite3/exec.hpp> | #include <neo/sqlite3/exec.hpp> | ||||
#include <neo/sqlite3/iter_tuples.hpp> | #include <neo/sqlite3/iter_tuples.hpp> | ||||
nsql::database db; | nsql::database db; | ||||
static remote_db download_and_open(neo::url const& url) { | static remote_db download_and_open(neo::url const& url) { | ||||
neo_assert(expects, | |||||
url.host.has_value(), | |||||
"URL does not have a hostname??", | |||||
url.to_string()); | |||||
auto sess = http_session::connect_for(url); | |||||
auto tempdir = temporary_dir::create(); | |||||
auto repo_db_dl = tempdir.path() / "repo.db"; | |||||
http_pool pool; | |||||
auto [client, resp] = pool.request_with_redirects("GET", url); | |||||
auto tempdir = temporary_dir::create(); | |||||
auto repo_db_dl = tempdir.path() / "repo.db"; | |||||
fs::create_directories(tempdir.path()); | fs::create_directories(tempdir.path()); | ||||
sess.download_file( | |||||
{ | |||||
.method = "GET", | |||||
.path = url.path, | |||||
}, | |||||
repo_db_dl); | |||||
auto outfile = neo::file_stream::open(repo_db_dl, neo::open_mode::write); | |||||
client.recv_body_into(resp, neo::stream_io_buffers(outfile)); | |||||
auto db = nsql::open(repo_db_dl.string()); | auto db = nsql::open(repo_db_dl.string()); | ||||
return {tempdir, std::move(db)}; | return {tempdir, std::move(db)}; | ||||
auto [remote_id] = nsql::unpack_single<std::int64_t>(rid_st); | auto [remote_id] = nsql::unpack_single<std::int64_t>(rid_st); | ||||
rid_st.reset(); | rid_st.reset(); | ||||
dds_log(trace, "Attaching downloaded database"); | |||||
nsql::exec(db.prepare("ATTACH DATABASE ? AS remote"), db_path.string()); | nsql::exec(db.prepare("ATTACH DATABASE ? AS remote"), db_path.string()); | ||||
neo_defer { db.exec("DETACH DATABASE remote"); }; | neo_defer { db.exec("DETACH DATABASE remote"); }; | ||||
nsql::transaction_guard tr{db}; | nsql::transaction_guard tr{db}; | ||||
dds_log(trace, "Clearing prior contents"); | |||||
nsql::exec( // | nsql::exec( // | ||||
db.prepare(R"( | db.prepare(R"( | ||||
DELETE FROM dds_cat_pkgs | DELETE FROM dds_cat_pkgs | ||||
WHERE remote_id = ? | WHERE remote_id = ? | ||||
)"), | )"), | ||||
remote_id); | remote_id); | ||||
dds_log(trace, "Importing packages"); | |||||
nsql::exec( // | nsql::exec( // | ||||
db.prepare(R"( | db.prepare(R"( | ||||
INSERT INTO dds_cat_pkgs | INSERT INTO dds_cat_pkgs | ||||
)"), | )"), | ||||
remote_id, | remote_id, | ||||
base_url_str); | base_url_str); | ||||
dds_log(trace, "Importing dependencies"); | |||||
db.exec(R"( | db.exec(R"( | ||||
INSERT OR REPLACE INTO dds_cat_pkg_deps (pkg_id, dep_name, low, high) | INSERT OR REPLACE INTO dds_cat_pkg_deps (pkg_id, dep_name, low, high) | ||||
SELECT | SELECT | ||||
dds_cat_pkgs AS local_pkgs USING(name, version) | dds_cat_pkgs AS local_pkgs USING(name, version) | ||||
)"); | )"); | ||||
// Validate our database | // Validate our database | ||||
auto fk_check = db.prepare("PRAGMA foreign_key_check"); | |||||
dds_log(trace, "Running integrity check"); | |||||
auto fk_check = db.prepare("PRAGMA foreign_key_check"); | |||||
auto rows = nsql::iter_tuples<std::string, std::int64_t, std::string, std::string>(fk_check); | auto rows = nsql::iter_tuples<std::string, std::int64_t, std::string, std::string>(fk_check); | ||||
bool any_failed = false; | bool any_failed = false; | ||||
for (auto [child_table, rowid, parent_table, failed_idx] : rows) { | for (auto [child_table, rowid, parent_table, failed_idx] : rows) { |
#include "./pool.hpp" | |||||
#include <dds/error/errors.hpp> | |||||
#include <dds/util/result.hpp> | |||||
#include <boost/leaf/exception.hpp> | |||||
#include <fmt/format.h> | |||||
#include <neo/gzip_io.hpp> | |||||
#include <neo/http/parse/chunked.hpp> | |||||
#include <neo/http/request.hpp> | |||||
#include <neo/http/response.hpp> | |||||
#include <neo/io/openssl/engine.hpp> | |||||
#include <neo/io/stream/buffers.hpp> | |||||
#include <neo/io/stream/socket.hpp> | |||||
#include <map> | |||||
namespace dds::detail { | |||||
struct http_client_impl { | |||||
network_origin origin; | |||||
explicit http_client_impl(network_origin o) | |||||
: origin(std::move(o)) {} | |||||
enum class _state_t { | |||||
ready, | |||||
sent_req_head, | |||||
sent_req_body, | |||||
recvd_resp_head, | |||||
}; | |||||
_state_t _state = _state_t::ready; | |||||
neo::socket _conn; | |||||
std::string _host_string; | |||||
using sock_buffers = neo::stream_io_buffers<neo::socket&>; | |||||
sock_buffers _sock_in{_conn}; | |||||
using ssl_engine = neo::ssl::engine<sock_buffers&, sock_buffers>; | |||||
using ssl_buffers = neo::stream_io_buffers<ssl_engine>; | |||||
std::optional<ssl_buffers> _ssl_in; | |||||
template <typename Fun> | |||||
auto _do_io(Fun&& fn) { | |||||
if (_ssl_in.has_value()) { | |||||
return fn(*_ssl_in); | |||||
} else { | |||||
return fn(_sock_in); | |||||
} | |||||
} | |||||
void connect() { | |||||
DDS_E_SCOPE(origin); | |||||
auto addr = neo::address::resolve(origin.hostname, std::to_string(origin.port)); | |||||
auto sock = neo::socket::open_connected(addr, neo::socket::type::stream); | |||||
_conn = std::move(sock); | |||||
if (origin.protocol == "https") { | |||||
static neo::ssl::openssl_app_init ssl_init; | |||||
static neo::ssl::context ssl_ctx{neo::ssl::protocol::tls_any, neo::ssl::role::client}; | |||||
_ssl_in.emplace(ssl_engine{ssl_ctx, _sock_in, neo::stream_io_buffers{_conn}}); | |||||
_ssl_in->stream().connect(); | |||||
} else if (origin.protocol == "http") { | |||||
// Plain HTTP, nothing special to do | |||||
} else { | |||||
throw_user_error<errc::invalid_remote_url>("Unknown protocol: {}", origin.protocol); | |||||
} | |||||
} | |||||
void send_head(const http_request_params& params) { | |||||
neo_assert(invariant, | |||||
_state == _state_t::ready, | |||||
"Invalid state for http_client::send_head()", | |||||
int(_state), | |||||
params.method, | |||||
params.path, | |||||
params.query, | |||||
origin.hostname, | |||||
origin.protocol, | |||||
origin.port); | |||||
neo::http::request_line start_line{ | |||||
.method_view = params.method, | |||||
.target = neo::http::origin_form_target{ | |||||
.path_view = params.path, | |||||
.query_view = params.query, | |||||
.has_query = !params.query.empty(), | |||||
.parse_tail = {}, | |||||
}, | |||||
.http_version = neo::http::version::v1_1, | |||||
.parse_tail = {}, | |||||
}; | |||||
auto content_len_str = std::to_string(params.content_length); | |||||
auto hostname_port = fmt::format("{}:{}", origin.hostname, origin.port); | |||||
std::pair<std::string_view, std::string_view> headers[] = { | |||||
{"Host", hostname_port}, | |||||
{"Accept", "*/*"}, | |||||
{"Content-Length", content_len_str}, | |||||
{"TE", "gzip, chunked, plain"}, | |||||
{"Connection", "keep-alive"}, | |||||
}; | |||||
_do_io([&](auto&& sink) { | |||||
neo::http::write_request(sink, start_line, headers, neo::const_buffer()); | |||||
}); | |||||
_state = _state_t::sent_req_head; | |||||
if (params.content_length == 0) { | |||||
_state = _state_t::sent_req_body; | |||||
} | |||||
} | |||||
http_response_info recv_head() { | |||||
neo_assert(invariant, | |||||
_state == _state_t::sent_req_body, | |||||
"Invalid state for http_client::recv_head()", | |||||
int(_state), | |||||
origin.hostname, | |||||
origin.protocol, | |||||
origin.port); | |||||
auto r = _do_io([&](auto&& source) { | |||||
return neo::http::read_response_head<http_response_info>(source); | |||||
}); | |||||
_state = _state_t::recvd_resp_head; | |||||
auto clen_hdr = r.headers.find(neo::http::standard_headers::content_length); | |||||
if (clen_hdr && clen_hdr->value == "0") { | |||||
_state = _state_t::ready; | |||||
} | |||||
return r; | |||||
} | |||||
}; | |||||
struct origin_order { | |||||
bool operator()(const network_origin& left, const network_origin& right) const noexcept { | |||||
return std::tie(left.protocol, left.hostname, left.port) | |||||
< std::tie(right.protocol, right.hostname, right.port); | |||||
} | |||||
}; | |||||
struct http_pool_impl { | |||||
std::multimap<network_origin, std::shared_ptr<http_client_impl>, origin_order> _clients; | |||||
}; | |||||
} // namespace dds::detail | |||||
using namespace dds; | |||||
http_pool::~http_pool() = default; | |||||
http_pool::http_pool() | |||||
: _impl(new detail::http_pool_impl) {} | |||||
http_client::~http_client() { | |||||
// When the http_client is dropped, return its impl back to the connection pool for this origin | |||||
auto pool = _pool.lock(); | |||||
if (pool && _impl) { | |||||
pool->_clients.emplace(_impl->origin, _impl); | |||||
} | |||||
} | |||||
network_origin network_origin::for_url(neo::url_view url) noexcept { | |||||
auto proto = url.scheme; | |||||
auto host = url.host.value_or(""); | |||||
auto port = url.port.value_or(proto == "https" ? 443 : 80); | |||||
return {std::string(proto), std::string(host), port}; | |||||
} | |||||
network_origin network_origin::for_url(neo::url const& url) noexcept { | |||||
auto proto = url.scheme; | |||||
auto host = url.host.value_or(""); | |||||
auto port = url.port.value_or(proto == "https" ? 443 : 80); | |||||
return {std::string(proto), std::string(host), port}; | |||||
} | |||||
http_client http_pool::client_for_origin(const network_origin& origin) { | |||||
auto iter = _impl->_clients.find(origin); | |||||
http_client ret; | |||||
ret._pool = _impl; | |||||
if (iter == _impl->_clients.end()) { | |||||
// Nothing for this origin yet | |||||
auto ptr = std::make_shared<detail::http_client_impl>(origin); | |||||
ptr->connect(); | |||||
ret._impl = ptr; | |||||
} else { | |||||
ret._impl = iter->second; | |||||
_impl->_clients.erase(iter); | |||||
} | |||||
return ret; | |||||
} | |||||
void http_client::send_head(const http_request_params& params) { _impl->send_head(params); } | |||||
http_response_info http_client::recv_head() { return _impl->recv_head(); } | |||||
void http_client::_send_buf(neo::const_buffer cbuf) { | |||||
_impl->_do_io([&](auto&& sink) { buffer_copy(sink, cbuf); }); | |||||
} | |||||
namespace { | |||||
struct recv_none_state : erased_message_body { | |||||
neo::const_buffer next(std::size_t) override { return {}; } | |||||
void consume(std::size_t) override {} | |||||
}; | |||||
template <typename Stream> | |||||
struct recv_chunked_state : erased_message_body { | |||||
Stream& _strm; | |||||
neo::http::chunked_buffers<Stream&> _chunked{_strm}; | |||||
explicit recv_chunked_state(Stream& s) | |||||
: _strm(s) {} | |||||
neo::const_buffer next(std::size_t n) override { return _chunked.next(n); } | |||||
void consume(std::size_t n) override { _chunked.consume(n); } | |||||
}; | |||||
template <typename Stream> | |||||
struct recv_gzip_state : erased_message_body { | |||||
Stream& _strm; | |||||
neo::gzip_source<Stream&> _gzip{_strm}; | |||||
explicit recv_gzip_state(Stream& s) | |||||
: _strm(s) {} | |||||
neo::const_buffer next(std::size_t n) override { return _gzip.next(n); } | |||||
void consume(std::size_t n) override { _gzip.consume(n); } | |||||
}; | |||||
template <typename Stream> | |||||
struct recv_plain_state : erased_message_body { | |||||
Stream& _strm; | |||||
std::size_t _size; | |||||
client_impl_ptr _client; | |||||
explicit recv_plain_state(Stream& s, std::size_t size) | |||||
: _strm(s) | |||||
, _size(size) {} | |||||
neo::const_buffer next(std::size_t n) override { return _strm.next((std::min)(n, _size)); } | |||||
void consume(std::size_t n) override { | |||||
_size -= n; | |||||
return _strm.consume(n); | |||||
} | |||||
}; | |||||
} // namespace | |||||
std::unique_ptr<erased_message_body> http_client::_make_body_reader(const http_response_info& res) { | |||||
neo_assert( | |||||
expects, | |||||
_impl->_state == detail::http_client_impl::_state_t::recvd_resp_head, | |||||
"Invalid state to ready HTTP response body. Have not yet received the response header", | |||||
int(_impl->_state), | |||||
_impl->origin.protocol, | |||||
_impl->origin.hostname, | |||||
_impl->origin.port); | |||||
if (res.status < 200 || res.status == 204 || res.status == 304) { | |||||
return std::make_unique<recv_none_state>(); | |||||
} | |||||
return _impl->_do_io([&](auto&& source) -> std::unique_ptr<erased_message_body> { | |||||
using source_type = decltype(source); | |||||
if (res.content_length() == 0) { | |||||
return std::make_unique<recv_none_state>(); | |||||
} else if (res.transfer_encoding() == "chunked") { | |||||
return std::make_unique<recv_chunked_state<source_type>>(source); | |||||
} else if (res.transfer_encoding() == "gzip") { | |||||
return std::make_unique<recv_gzip_state<source_type>>(source); | |||||
} else if (!res.transfer_encoding().has_value() && res.content_length() > 0) { | |||||
return std::make_unique<recv_plain_state<source_type>>(source, *res.content_length()); | |||||
} else { | |||||
neo_assert(invariant, | |||||
false, | |||||
"Unimplemented", | |||||
res.transfer_encoding().value_or("[null]")); | |||||
} | |||||
}); | |||||
} | |||||
void http_client::discard_body(const http_response_info& resp) { | |||||
auto reader_ = _make_body_reader(resp); | |||||
auto& reader = *reader_; | |||||
while (true) { | |||||
auto part = reader.next(1024); | |||||
reader.consume(neo::buffer_size(part)); | |||||
if (neo::buffer_is_empty(part)) { | |||||
break; | |||||
} | |||||
} | |||||
_set_ready(); | |||||
} | |||||
void http_client::_set_ready() noexcept { | |||||
_impl->_state = detail::http_client_impl::_state_t::ready; | |||||
} | |||||
std::pair<http_client, http_response_info> | |||||
http_pool::request_with_redirects(std::string_view method, const neo::url& url_) { | |||||
auto url = url_; | |||||
DDS_E_SCOPE(url); | |||||
for (auto i = 0; i <= 100; ++i) { | |||||
auto origin = network_origin::for_url(url); | |||||
auto client = client_for_origin(origin); | |||||
http_request_params params{ | |||||
.method = method, | |||||
.path = url.path, | |||||
.query = url.query.value_or(""), | |||||
}; | |||||
client.send_head(params); | |||||
auto resp = client.recv_head(); | |||||
DDS_E_SCOPE(resp); | |||||
if (resp.is_error()) { | |||||
client.discard_body(resp); | |||||
throw boost::leaf::exception(http_status_error("Received an error from HTTP")); | |||||
} | |||||
if (resp.is_redirect()) { | |||||
client.discard_body(resp); | |||||
if (i == 100) { | |||||
throw boost::leaf::exception( | |||||
http_server_error("Encountered over 100 HTTP redirects. Request aborted.")); | |||||
} | |||||
auto loc = resp.headers.find("Location"); | |||||
if (!loc) { | |||||
throw boost::leaf::exception( | |||||
http_server_error("Server sent an invalid response of a 30x redirect without a " | |||||
"'Location' header")); | |||||
} | |||||
url = neo::url::parse(loc->value); | |||||
continue; | |||||
} | |||||
return {std::move(client), std::move(resp)}; | |||||
} | |||||
neo::unreachable(); | |||||
} |
#pragma once | |||||
#include "./request.hpp" | |||||
#include "./response.hpp" | |||||
#include <neo/buffer_algorithm/copy.hpp> | |||||
#include <neo/buffer_sink.hpp> | |||||
#include <neo/buffer_source.hpp> | |||||
#include <neo/url.hpp> | |||||
#include <neo/url/view.hpp> | |||||
#include <neo/utility.hpp> | |||||
#include <memory> | |||||
namespace dds { | |||||
namespace detail { | |||||
struct http_pool_access_impl; | |||||
struct http_pool_impl; | |||||
struct http_client_impl; | |||||
} // namespace detail | |||||
struct erased_message_body { | |||||
virtual ~erased_message_body() = default; | |||||
virtual neo::const_buffer next(std::size_t n) = 0; | |||||
virtual void consume(std::size_t n) = 0; | |||||
}; | |||||
class http_status_error : public std::runtime_error { | |||||
using runtime_error::runtime_error; | |||||
}; | |||||
class http_server_error : public std::runtime_error { | |||||
using runtime_error::runtime_error; | |||||
}; | |||||
struct network_origin { | |||||
std::string protocol; | |||||
std::string hostname; | |||||
int port = 0; | |||||
static network_origin for_url(neo::url_view url) noexcept; | |||||
static network_origin for_url(const neo::url& url) noexcept; | |||||
}; | |||||
class http_client { | |||||
friend class http_pool; | |||||
std::weak_ptr<detail::http_pool_impl> _pool; | |||||
std::shared_ptr<detail::http_client_impl> _impl; | |||||
http_client() = default; | |||||
void _send_buf(neo::const_buffer); | |||||
std::unique_ptr<erased_message_body> _make_body_reader(const http_response_info&); | |||||
void _set_ready() noexcept; | |||||
public: | |||||
http_client(http_client&& o) | |||||
: _pool(neo::take(o._pool)) | |||||
, _impl(neo::take(o._impl)) {} | |||||
~http_client(); | |||||
void send_head(http_request_params const& params); | |||||
http_response_info recv_head(); | |||||
template <neo::buffer_input Body> | |||||
void send_body(Body&& body) { | |||||
if constexpr (neo::single_buffer<Body>) { | |||||
_send_buf(body); | |||||
} else if constexpr (neo::buffer_range<Body>) { | |||||
neo::buffers_consumer cons{body}; | |||||
send_body(cons); | |||||
} else { | |||||
while (true) { | |||||
auto part = body.next(1024); | |||||
if (neo::buffer_is_empty(part)) { | |||||
break; | |||||
} | |||||
send_body(part); | |||||
body.consume(neo::buffer_size(part)); | |||||
} | |||||
} | |||||
} | |||||
template <neo::buffer_output Out> | |||||
void recv_body_into(const http_response_info& resp, Out&& out) { | |||||
auto&& sink = neo::ensure_buffer_sink(out); | |||||
auto state = _make_body_reader(resp); | |||||
neo::buffer_copy(sink, *state); | |||||
_set_ready(); | |||||
} | |||||
void discard_body(const http_response_info&); | |||||
}; | |||||
class http_pool { | |||||
friend class http_client; | |||||
std::shared_ptr<detail::http_pool_impl> _impl; | |||||
public: | |||||
http_pool(); | |||||
http_pool(http_pool&&) = default; | |||||
http_pool& operator=(http_pool&&) = default; | |||||
~http_pool(); | |||||
http_client client_for_origin(const network_origin&); | |||||
http_response_info request(neo::url_view url) { return request(url, neo::mutable_buffer()); } | |||||
template <neo::buffer_output Output> | |||||
http_response_info request(neo::url_view url, Output&& out) { | |||||
return request(url, neo::const_buffer(), out); | |||||
} | |||||
template <neo::buffer_input In, neo::buffer_output Out> | |||||
http_response_info request(neo::url_view url, In&& in, Out&& out) { | |||||
auto origin = network_origin::for_url(url); | |||||
auto size = neo::buffer_size(in); | |||||
auto client = client_for_origin(origin); | |||||
client.send_head(http_request_params{ | |||||
.method = "GET", | |||||
.path = url.path.empty() ? "/" : url.path, | |||||
.query = url.query.value_or(""), | |||||
.content_length = size, | |||||
}); | |||||
client.send_body(in); | |||||
auto resp = client.recv_head(); | |||||
client.recv_body_into(resp, out); | |||||
return resp; | |||||
} | |||||
std::pair<http_client, http_response_info> | |||||
request_with_redirects(http_client& cl, const http_request_params& params); | |||||
std::pair<http_client, http_response_info> request_with_redirects(std::string_view method, | |||||
const neo::url& url); | |||||
}; | |||||
} // namespace dds |
#include "./pool.hpp" | |||||
#include <neo/string_io.hpp> | |||||
#include <neo/url.hpp> | |||||
#include <catch2/catch.hpp> | |||||
TEST_CASE("Create an empty pool") { dds::http_pool pool; } | |||||
TEST_CASE("Connect to a remote") { | |||||
dds::http_pool pool; | |||||
// auto client = pool.access(); | |||||
auto cl = pool.client_for_origin({"https", "www.google.com", 443}); | |||||
cl.send_head({.method = "GET", .path = "/"}); | |||||
// cl.send_head({.method = "GET", .path = "/"}); | |||||
auto resp = cl.recv_head(); | |||||
CHECK(resp.status == 200); | |||||
CHECK(resp.status_message == "OK"); | |||||
} | |||||
TEST_CASE("Issue a request on a pool") { | |||||
dds::http_pool pool; | |||||
neo::string_dynbuf_io body; | |||||
auto resp = pool.request(neo::url_view::split("https://www.google.com"), body); | |||||
CHECK(resp.status == 200); | |||||
CHECK(body.read_area_view().size() > 5); | |||||
} |
#pragma once | |||||
#include <string_view> | |||||
#include <neo/http/headers.hpp> | |||||
namespace dds { | |||||
struct http_request_params { | |||||
std::string_view method; | |||||
std::string_view path; | |||||
std::string_view query = ""; | |||||
std::size_t content_length = 0; | |||||
neo::http::headers headers{}; | |||||
}; | |||||
} // namespace dds |
#include "./response.hpp" | |||||
#include <dds/util/log.hpp> | |||||
#include <neo/http/parse/header.hpp> | |||||
#include <charconv> | |||||
using namespace dds; | |||||
std::optional<int> http_response_info::content_length() const noexcept { | |||||
auto cl_str = header_value("Content-Length"); | |||||
if (!cl_str) { | |||||
return {}; | |||||
} | |||||
int clen = 0; | |||||
auto conv_res = std::from_chars(cl_str->data(), cl_str->data() + cl_str->size(), clen); | |||||
if (conv_res.ec != std::errc{}) { | |||||
dds_log(warn, | |||||
"The HTTP server returned a non-integral 'Content-Length' header: '{}'. We'll " | |||||
"pretend that there is no 'Content-Length' on this message.", | |||||
*cl_str); | |||||
return {}; | |||||
} | |||||
return clen; | |||||
} | |||||
std::optional<std::string_view> http_response_info::header_value(std::string_view key) const noexcept { | |||||
auto hdr = headers.find(key); | |||||
if (!hdr) { | |||||
return {}; | |||||
} | |||||
return hdr->value; | |||||
} |
#pragma once | |||||
#include <neo/http/headers.hpp> | |||||
#include <neo/http/version.hpp> | |||||
#include <string> | |||||
namespace dds { | |||||
struct http_response_info { | |||||
int status; | |||||
std::string status_message; | |||||
neo::http::version version; | |||||
neo::http::headers headers; | |||||
std::size_t head_byte_size = 0; | |||||
void throw_for_status() const; | |||||
bool is_client_error() const noexcept { return status >= 400 && status < 500; } | |||||
bool is_server_error() const noexcept { return status >= 500 && status < 600; } | |||||
bool is_error() const noexcept { return is_client_error() || is_server_error(); } | |||||
bool is_redirect() const noexcept { return status >= 300 && status < 400; } | |||||
std::optional<std::string_view> header_value(std::string_view key) const noexcept; | |||||
std::optional<int> content_length() const noexcept; | |||||
auto location() const noexcept { return header_value("Location"); } | |||||
auto transfer_encoding() const noexcept { return header_value("Transfer-Encoding"); } | |||||
}; | |||||
} // namespace dds |