Pull neo-io and neo-http fix: failbit will cause exceptions on EOFdefault_compile_flags
| @@ -1841,6 +1841,16 @@ | |||
| "description": "Compression, archiving, etc. for C++20", | |||
| "transform": [], | |||
| "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": { | |||
| @@ -1953,6 +1963,34 @@ | |||
| "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.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": { | |||
| @@ -2228,6 +2228,19 @@ | |||
| "transform": [], | |||
| "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": { | |||
| @@ -2394,6 +2407,43 @@ | |||
| "transform": [], | |||
| "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": { | |||
| @@ -13,7 +13,8 @@ | |||
| "vob/json5", | |||
| "vob/semester", | |||
| "hanickadot/ctre", | |||
| // "neo/io", | |||
| "neo/io", | |||
| "neo/http", | |||
| "neo/url", | |||
| // Explicit zlib link is required due to linker input order bug. | |||
| // Can be removed after alpha.5 | |||
| @@ -9,15 +9,17 @@ | |||
| "range-v3@0.11.0", | |||
| "nlohmann-json@3.7.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", | |||
| "semver@0.2.2", | |||
| "pubgrub@0.2.1", | |||
| "vob-json5@0.1.5", | |||
| "vob-semester@0.2.2", | |||
| "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" | |||
| } | |||
| @@ -39,6 +39,8 @@ std::string error_url_suffix(dds::errc ec) noexcept { | |||
| return "git-clone-failure.html"; | |||
| case errc::invalid_remote_url: | |||
| return "invalid-remote-url.html"; | |||
| case errc::http_download_failure: | |||
| return "http-failure.html"; | |||
| case errc::invalid_repo_transform: | |||
| return "invalid-repo-transform.html"; | |||
| case errc::sdist_ident_mismatch: | |||
| @@ -176,6 +178,12 @@ Git in diagnosing this failure. | |||
| )"; | |||
| case errc::invalid_remote_url: | |||
| 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: | |||
| return R"( | |||
| A 'transform' property in a catalog entry contains an invalid transformation. | |||
| @@ -290,6 +298,8 @@ std::string_view dds::default_error_string(dds::errc ec) noexcept { | |||
| return "A git-clone operation failed."; | |||
| case errc::invalid_remote_url: | |||
| 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: | |||
| return "A repository filesystem transformation is invalid"; | |||
| case errc::sdist_ident_mismatch: | |||
| @@ -25,6 +25,7 @@ enum class errc { | |||
| git_clone_failure, | |||
| invalid_remote_url, | |||
| http_download_failure, | |||
| invalid_repo_transform, | |||
| sdist_ident_mismatch, | |||
| sdist_exists, | |||
| @@ -0,0 +1,160 @@ | |||
| #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); | |||
| } | |||
| @@ -0,0 +1,121 @@ | |||
| #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 | |||
| @@ -0,0 +1,8 @@ | |||
| #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("/"); | |||
| } | |||
| @@ -8,7 +8,7 @@ using namespace dds; | |||
| std::fstream dds::open(const fs::path& filepath, std::ios::openmode mode, std::error_code& ec) { | |||
| std::fstream ret; | |||
| auto mask = ret.exceptions() | std::ios::failbit; | |||
| auto mask = ret.exceptions() | std::ios::badbit; | |||
| ret.exceptions(mask); | |||
| try { | |||
| @@ -291,14 +291,16 @@ def many_versions(name: str, | |||
| PACKAGES = [ | |||
| github_package('neo-buffer', 'vector-of-bool/neo-buffer', | |||
| ['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-sqlite3', 'vector-of-bool/neo-sqlite3', | |||
| ['0.2.3', '0.3.0', '0.4.0', '0.4.1']), | |||
| 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.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', ( | |||
| '0.2.2', | |||
| '0.3.0', | |||