Pull neo-io and neo-http fix: failbit will cause exceptions on EOFdefault_compile_flags
"description": "Compression, archiving, etc. for C++20", | "description": "Compression, archiving, etc. for C++20", | ||||
"transform": [], | "transform": [], | ||||
"url": "git+https://github.com/vector-of-bool/neo-compress.git#0.1.0" | "url": "git+https://github.com/vector-of-bool/neo-compress.git#0.1.0" | ||||
}, | |||||
"0.1.1": { | |||||
"depends": [ | |||||
"neo-buffer^0.4.1", | |||||
"neo-fun^0.5.0", | |||||
"zlib^1.2.9" | |||||
], | |||||
"description": "Compression, archiving, etc. for C++20", | |||||
"transform": [], | |||||
"url": "git+https://github.com/vector-of-bool/neo-compress.git#0.1.1" | |||||
} | } | ||||
}, | }, | ||||
"neo-concepts": { | "neo-concepts": { | ||||
"description": "Some library components that didn't quite fit anywhere else...", | "description": "Some library components that didn't quite fit anywhere else...", | ||||
"transform": [], | "transform": [], | ||||
"url": "git+https://github.com/vector-of-bool/neo-fun.git#0.5.3" | "url": "git+https://github.com/vector-of-bool/neo-fun.git#0.5.3" | ||||
}, | |||||
"0.5.4": { | |||||
"depends": [], | |||||
"description": "Some library components that didn't quite fit anywhere else...", | |||||
"transform": [], | |||||
"url": "git+https://github.com/vector-of-bool/neo-fun.git#0.5.4" | |||||
} | |||||
}, | |||||
"neo-http": { | |||||
"0.1.0": { | |||||
"depends": [ | |||||
"neo-buffer^0.4.2", | |||||
"neo-fun^0.5.4" | |||||
], | |||||
"description": "A modern HTTP library", | |||||
"transform": [], | |||||
"url": "git+https://github.com/vector-of-bool/neo-http.git#0.1.0" | |||||
} | |||||
}, | |||||
"neo-io": { | |||||
"0.1.0": { | |||||
"depends": [ | |||||
"neo-fun~0.5.4", | |||||
"neo-buffer~0.4.2" | |||||
], | |||||
"description": "A modern IO library", | |||||
"transform": [], | |||||
"url": "git+https://github.com/vector-of-bool/neo-io.git#0.1.0" | |||||
} | } | ||||
}, | }, | ||||
"neo-sqlite3": { | "neo-sqlite3": { |
"transform": [], | "transform": [], | ||||
"url": "https://github.com/vector-of-bool/neo-compress.git" | "url": "https://github.com/vector-of-bool/neo-compress.git" | ||||
} | } | ||||
}, | |||||
"0.1.1": { | |||||
"depends": [ | |||||
"neo-buffer^0.4.1", | |||||
"neo-fun^0.5.0", | |||||
"zlib^1.2.9" | |||||
], | |||||
"description": "Compression, archiving, etc. for C++20", | |||||
"git": { | |||||
"ref": "0.1.1", | |||||
"transform": [], | |||||
"url": "https://github.com/vector-of-bool/neo-compress.git" | |||||
} | |||||
} | } | ||||
}, | }, | ||||
"neo-concepts": { | "neo-concepts": { | ||||
"transform": [], | "transform": [], | ||||
"url": "https://github.com/vector-of-bool/neo-fun.git" | "url": "https://github.com/vector-of-bool/neo-fun.git" | ||||
} | } | ||||
}, | |||||
"0.5.4": { | |||||
"depends": [], | |||||
"description": "Some library components that didn't quite fit anywhere else...", | |||||
"git": { | |||||
"ref": "0.5.4", | |||||
"transform": [], | |||||
"url": "https://github.com/vector-of-bool/neo-fun.git" | |||||
} | |||||
} | |||||
}, | |||||
"neo-http": { | |||||
"0.1.0": { | |||||
"depends": [ | |||||
"neo-buffer^0.4.2", | |||||
"neo-fun^0.5.4" | |||||
], | |||||
"description": "A modern HTTP library", | |||||
"git": { | |||||
"ref": "0.1.0", | |||||
"transform": [], | |||||
"url": "https://github.com/vector-of-bool/neo-http.git" | |||||
} | |||||
} | |||||
}, | |||||
"neo-io": { | |||||
"0.1.0": { | |||||
"depends": [ | |||||
"neo-fun~0.5.4", | |||||
"neo-buffer~0.4.2" | |||||
], | |||||
"description": "A modern IO library", | |||||
"git": { | |||||
"ref": "0.1.0", | |||||
"transform": [], | |||||
"url": "https://github.com/vector-of-bool/neo-io.git" | |||||
} | |||||
} | } | ||||
}, | }, | ||||
"neo-sqlite3": { | "neo-sqlite3": { |
"vob/json5", | "vob/json5", | ||||
"vob/semester", | "vob/semester", | ||||
"hanickadot/ctre", | "hanickadot/ctre", | ||||
// "neo/io", | |||||
"neo/io", | |||||
"neo/http", | |||||
"neo/url", | "neo/url", | ||||
// Explicit zlib link is required due to linker input order bug. | // Explicit zlib link is required due to linker input order bug. | ||||
// Can be removed after alpha.5 | // Can be removed after alpha.5 |
"range-v3@0.11.0", | "range-v3@0.11.0", | ||||
"nlohmann-json@3.7.1", | "nlohmann-json@3.7.1", | ||||
"neo-sqlite3@0.4.1", | "neo-sqlite3@0.4.1", | ||||
"neo-fun~0.5.3", | |||||
"neo-compress^0.1.0", | |||||
"neo-fun~0.5.4", | |||||
"neo-compress~0.1.1", | |||||
"neo-url~0.1.2", | "neo-url~0.1.2", | ||||
"semver@0.2.2", | "semver@0.2.2", | ||||
"pubgrub@0.2.1", | "pubgrub@0.2.1", | ||||
"vob-json5@0.1.5", | "vob-json5@0.1.5", | ||||
"vob-semester@0.2.2", | "vob-semester@0.2.2", | ||||
"ctre@2.8.1", | "ctre@2.8.1", | ||||
"fmt^7.0.3" | |||||
"fmt^7.0.3", | |||||
"neo-http^0.1.0", | |||||
"neo-io^0.1.0", | |||||
], | ], | ||||
"test_driver": "Catch-Main" | "test_driver": "Catch-Main" | ||||
} | } |
return "git-clone-failure.html"; | return "git-clone-failure.html"; | ||||
case errc::invalid_remote_url: | case errc::invalid_remote_url: | ||||
return "invalid-remote-url.html"; | return "invalid-remote-url.html"; | ||||
case errc::http_download_failure: | |||||
return "http-failure.html"; | |||||
case errc::invalid_repo_transform: | case errc::invalid_repo_transform: | ||||
return "invalid-repo-transform.html"; | return "invalid-repo-transform.html"; | ||||
case errc::sdist_ident_mismatch: | case errc::sdist_ident_mismatch: | ||||
)"; | )"; | ||||
case errc::invalid_remote_url: | case errc::invalid_remote_url: | ||||
return R"(The given package/remote URL is invalid)"; | return R"(The given package/remote URL is invalid)"; | ||||
case errc::http_download_failure: | |||||
return R"( | |||||
There was a problem when trying to download data from an HTTP server. HTTP 40x | |||||
errors indicate problems on the client-side, and HTTP 50x errors indicate that | |||||
the server itself encountered an error. | |||||
)"; | |||||
case errc::invalid_repo_transform: | case errc::invalid_repo_transform: | ||||
return R"( | return R"( | ||||
A 'transform' property in a catalog entry contains an invalid transformation. | A 'transform' property in a catalog entry contains an invalid transformation. | ||||
return "A git-clone operation failed."; | return "A git-clone operation failed."; | ||||
case errc::invalid_remote_url: | case errc::invalid_remote_url: | ||||
return "The given package/remote URL is not valid"; | return "The given package/remote URL is not valid"; | ||||
case errc::http_download_failure: | |||||
return "There was an error downloading data from an HTTP server."; | |||||
case errc::invalid_repo_transform: | case errc::invalid_repo_transform: | ||||
return "A repository filesystem transformation is invalid"; | return "A repository filesystem transformation is invalid"; | ||||
case errc::sdist_ident_mismatch: | case errc::sdist_ident_mismatch: |
git_clone_failure, | git_clone_failure, | ||||
invalid_remote_url, | invalid_remote_url, | ||||
http_download_failure, | |||||
invalid_repo_transform, | invalid_repo_transform, | ||||
sdist_ident_mismatch, | sdist_ident_mismatch, | ||||
sdist_exists, | sdist_exists, |
#include "./session.hpp" | |||||
#include <dds/error/errors.hpp> | |||||
#include <dds/util/fs.hpp> | |||||
#include <fmt/format.h> | |||||
#include <fmt/ostream.h> | |||||
#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(const std::string& host, int 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) { | |||||
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::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(), | |||||
}; | |||||
auto cl_str = std::to_string(params.content_length); | |||||
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); }); | |||||
_state = _state_t::recvd_head; | |||||
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); }); | |||||
auto body_size = resp_body.available(); | |||||
auto str = std::move(resp_body.string()); | |||||
str.resize(body_size); | |||||
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); }); | |||||
_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 <filesystem> | |||||
#include <string> | |||||
namespace dds { | |||||
struct http_request_params { | |||||
std::string_view method; | |||||
std::string_view path; | |||||
std::string_view query = ""; | |||||
std::size_t content_length = 0; | |||||
}; | |||||
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; } | |||||
}; | |||||
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); | |||||
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("/"); | |||||
} |
std::fstream dds::open(const fs::path& filepath, std::ios::openmode mode, std::error_code& ec) { | std::fstream dds::open(const fs::path& filepath, std::ios::openmode mode, std::error_code& ec) { | ||||
std::fstream ret; | std::fstream ret; | ||||
auto mask = ret.exceptions() | std::ios::failbit; | |||||
auto mask = ret.exceptions() | std::ios::badbit; | |||||
ret.exceptions(mask); | ret.exceptions(mask); | ||||
try { | try { |
PACKAGES = [ | PACKAGES = [ | ||||
github_package('neo-buffer', 'vector-of-bool/neo-buffer', | github_package('neo-buffer', 'vector-of-bool/neo-buffer', | ||||
['0.2.1', '0.3.0', '0.4.0', '0.4.1', '0.4.2']), | ['0.2.1', '0.3.0', '0.4.0', '0.4.1', '0.4.2']), | ||||
github_package('neo-compress', 'vector-of-bool/neo-compress', ['0.1.0']), | |||||
github_package('neo-compress', 'vector-of-bool/neo-compress', ['0.1.0', '0.1.1']), | |||||
github_package('neo-url', 'vector-of-bool/neo-url', ['0.1.0', '0.1.1', '0.1.2']), | github_package('neo-url', 'vector-of-bool/neo-url', ['0.1.0', '0.1.1', '0.1.2']), | ||||
github_package('neo-sqlite3', 'vector-of-bool/neo-sqlite3', | github_package('neo-sqlite3', 'vector-of-bool/neo-sqlite3', | ||||
['0.2.3', '0.3.0', '0.4.0', '0.4.1']), | ['0.2.3', '0.3.0', '0.4.0', '0.4.1']), | ||||
github_package('neo-fun', 'vector-of-bool/neo-fun', [ | github_package('neo-fun', 'vector-of-bool/neo-fun', [ | ||||
'0.1.1', '0.2.0', '0.2.1', '0.3.0', '0.3.1', '0.3.2', '0.4.0', '0.4.1', | '0.1.1', '0.2.0', '0.2.1', '0.3.0', '0.3.1', '0.3.2', '0.4.0', '0.4.1', | ||||
'0.4.2', '0.5.0', '0.5.1', '0.5.2', '0.5.3', | |||||
'0.4.2', '0.5.0', '0.5.1', '0.5.2', '0.5.3', '0.5.4', | |||||
]), | ]), | ||||
github_package('neo-io', 'vector-of-bool/neo-io', ['0.1.0']), | |||||
github_package('neo-http', 'vector-of-bool/neo-http', ['0.1.0']), | |||||
github_package('neo-concepts', 'vector-of-bool/neo-concepts', ( | github_package('neo-concepts', 'vector-of-bool/neo-concepts', ( | ||||
'0.2.2', | '0.2.2', | ||||
'0.3.0', | '0.3.0', |