@@ -2,13 +2,14 @@ | |||
#include <dds/dym.hpp> | |||
#include <dds/error/errors.hpp> | |||
#include <dds/http/session.hpp> | |||
#include <dds/pkg/db.hpp> | |||
#include <dds/pkg/get/get.hpp> | |||
#include <dds/util/http/pool.hpp> | |||
#include <dds/util/result.hpp> | |||
#include <boost/leaf/handle_exception.hpp> | |||
#include <json5/parse_data.hpp> | |||
#include <neo/url.hpp> | |||
namespace dds::cli::cmd { | |||
@@ -47,10 +48,10 @@ int pkg_get(const options& opts) { | |||
url_err.what()); | |||
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, | |||
"Error parsing JSON5 document package downloaded from [{}]: {}", | |||
bad_url.value, | |||
bad_url.to_string(), | |||
e.what()); | |||
return 1; | |||
}, | |||
@@ -58,10 +59,10 @@ int pkg_get(const options& opts) { | |||
dds_log(error, "Error accessing the package database: {}", e.message); | |||
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, | |||
"Error opening connection to [{}:{}]: {}", | |||
conn.host, | |||
conn.hostname, | |||
conn.port, | |||
e.message); | |||
return 1; |
@@ -1,6 +1,5 @@ | |||
#include "../options.hpp" | |||
#include <dds/http/session.hpp> | |||
#include <dds/pkg/cache.hpp> | |||
#include <dds/sdist/dist.hpp> | |||
#include <dds/util/result.hpp> |
@@ -1,12 +1,12 @@ | |||
#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/result.hpp> | |||
#include <boost/leaf/handle_exception.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) { | |||
return boost::leaf::try_catch( | |||
@@ -17,29 +17,39 @@ int dds::cli::cmd::handle_pkg_repo_remote_errors(std::function<int()> fn) { | |||
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; | |||
}, | |||
[&](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, | |||
"Error parsing JSON downloaded from URL [{}]: {}", | |||
bad_url.value, | |||
bad_url.to_string(), | |||
e.what()); | |||
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; | |||
}, | |||
[](dds::e_sqlite3_error_exc e) { | |||
dds_log(error, "Unexpected database error: {}", e.message); | |||
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, | |||
"Error opening connection to [{}:{}]: {}", | |||
conn.host, | |||
"Error communicating with [{}://{}:{}]: {}", | |||
conn.protocol, | |||
conn.hostname, | |||
conn.port, | |||
e.message); | |||
return 1; |
@@ -1,204 +0,0 @@ | |||
#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); | |||
} |
@@ -1,145 +0,0 @@ | |||
#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 |
@@ -1,8 +0,0 @@ | |||
#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("/"); | |||
} |
@@ -1,86 +1,18 @@ | |||
#include "./http.hpp" | |||
#include <dds/error/errors.hpp> | |||
#include <dds/http/session.hpp> | |||
#include <dds/temp.hpp> | |||
#include <dds/util/http/pool.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/url.hpp> | |||
#include <neo/url/query.hpp> | |||
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 { | |||
neo::url url; | |||
try { | |||
@@ -115,7 +47,10 @@ void http_remote_listing::pull_source(path_ref dest) const { | |||
auto dl_path = tdir.path() / fname; | |||
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, | |||
fs::is_regular_file(dl_path), |
@@ -1,12 +1,14 @@ | |||
#include "./remote.hpp" | |||
#include <dds/error/errors.hpp> | |||
#include <dds/http/session.hpp> | |||
#include <dds/temp.hpp> | |||
#include <dds/util/http/pool.hpp> | |||
#include <dds/util/log.hpp> | |||
#include <dds/util/result.hpp> | |||
#include <neo/event.hpp> | |||
#include <neo/io/stream/buffers.hpp> | |||
#include <neo/io/stream/file.hpp> | |||
#include <neo/scope.hpp> | |||
#include <neo/sqlite3/exec.hpp> | |||
#include <neo/sqlite3/iter_tuples.hpp> | |||
@@ -26,21 +28,14 @@ struct remote_db { | |||
nsql::database db; | |||
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()); | |||
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()); | |||
return {tempdir, std::move(db)}; | |||
@@ -97,15 +92,18 @@ void pkg_remote::update_pkg_db(nsql::database_ref db) { | |||
auto [remote_id] = nsql::unpack_single<std::int64_t>(rid_st); | |||
rid_st.reset(); | |||
dds_log(trace, "Attaching downloaded database"); | |||
nsql::exec(db.prepare("ATTACH DATABASE ? AS remote"), db_path.string()); | |||
neo_defer { db.exec("DETACH DATABASE remote"); }; | |||
nsql::transaction_guard tr{db}; | |||
dds_log(trace, "Clearing prior contents"); | |||
nsql::exec( // | |||
db.prepare(R"( | |||
DELETE FROM dds_cat_pkgs | |||
WHERE remote_id = ? | |||
)"), | |||
remote_id); | |||
dds_log(trace, "Importing packages"); | |||
nsql::exec( // | |||
db.prepare(R"( | |||
INSERT INTO dds_cat_pkgs | |||
@@ -128,6 +126,7 @@ void pkg_remote::update_pkg_db(nsql::database_ref db) { | |||
)"), | |||
remote_id, | |||
base_url_str); | |||
dds_log(trace, "Importing dependencies"); | |||
db.exec(R"( | |||
INSERT OR REPLACE INTO dds_cat_pkg_deps (pkg_id, dep_name, low, high) | |||
SELECT | |||
@@ -140,7 +139,8 @@ void pkg_remote::update_pkg_db(nsql::database_ref db) { | |||
dds_cat_pkgs AS local_pkgs USING(name, version) | |||
)"); | |||
// 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); | |||
bool any_failed = false; | |||
for (auto [child_table, rowid, parent_table, failed_idx] : rows) { |
@@ -0,0 +1,341 @@ | |||
#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(); | |||
} |
@@ -0,0 +1,145 @@ | |||
#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 |
@@ -0,0 +1,27 @@ | |||
#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); | |||
} |
@@ -0,0 +1,17 @@ | |||
#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 |
@@ -0,0 +1,34 @@ | |||
#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; | |||
} |
@@ -0,0 +1,32 @@ | |||
#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 |