From 32ebac16f9aef77ee4839e68915511291b0ba383 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Thu, 5 Feb 2026 19:22:47 -0500 Subject: [PATCH 1/8] Hack in test condition to discover expected ec. --- test/net/connector.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/net/connector.cpp b/test/net/connector.cpp index 792184783..7c30a772d 100644 --- a/test/net/connector.cpp +++ b/test/net/connector.cpp @@ -231,6 +231,8 @@ BOOST_AUTO_TEST_CASE(connector__connect__started_start__operation_failed) connector::parameters params{ .connect_timeout = seconds(1000), .maximum_request = 42 }; auto instance = std::make_shared(log, strand, pool.service(), suspended, std::move(params)); auto result = true; + code ec1{}; + code ec2{}; boost::asio::post(strand, [&, instance]() NOEXCEPT { @@ -238,6 +240,7 @@ BOOST_AUTO_TEST_CASE(connector__connect__started_start__operation_failed) instance->connect(config::endpoint{ "bogus.xxx", 42 }, [&](const code& ec, const socket::ptr& socket) NOEXCEPT { + ec1 = ec; result &= (((ec == error::resolve_failed) && !socket) || (ec == error::operation_canceled)); }); @@ -245,6 +248,7 @@ BOOST_AUTO_TEST_CASE(connector__connect__started_start__operation_failed) instance->connect(config::endpoint{ "bogus.yyy", 24 }, [&](const code& ec, const socket::ptr& socket) NOEXCEPT { + ec2 = ec; result &= (ec == error::operation_failed); result &= is_null(socket); }); @@ -256,6 +260,16 @@ BOOST_AUTO_TEST_CASE(connector__connect__started_start__operation_failed) pool.stop(); BOOST_REQUIRE(pool.join()); BOOST_REQUIRE(instance->get_stopped()); + + // BUGBUG: macOS: + // Error codes have changed and the resulting race failure test is not matching expected code. + if (!result) + { + BOOST_CHECK_EQUAL(ec1, error::resolve_failed); + BOOST_CHECK_EQUAL(ec1, error::operation_canceled); + BOOST_CHECK_EQUAL(ec2, error::operation_failed); + } + BOOST_REQUIRE(result); } From 2fe72e574be6102217dae21c97c36e236b87e6d5 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Fri, 6 Feb 2026 11:14:45 -0500 Subject: [PATCH 2/8] Update error::tls_unexpected_result text. --- src/error.cpp | 2 +- test/error.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/error.cpp b/src/error.cpp index 83ed61e52..24a41f377 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -132,7 +132,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) { tls_set_add_verify, "failed to set tls certificate authority" }, { tls_stream_truncated, "tls stream truncated" }, { tls_unspecified_system_error, "tls unspecified system error" }, - { tls_unexpected_result, "tls unexpected result" }, + { tls_unexpected_result, "tls handshake failure" }, // boost beast http 4xx client error { bad_request, "bad request" }, diff --git a/test/error.cpp b/test/error.cpp index af0d29fab..075448554 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -755,7 +755,7 @@ BOOST_AUTO_TEST_CASE(error_t__code__tls_unexpected_result__true_expected_message const auto ec = code(value); BOOST_REQUIRE(ec); BOOST_REQUIRE(ec == value); - BOOST_REQUIRE_EQUAL(ec.message(), "tls unexpected result"); + BOOST_REQUIRE_EQUAL(ec.message(), "tls handshake failure"); } // http 4xx client error From 955fef0e2e4555f929b301bf3eda54db6c545d88 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Fri, 6 Feb 2026 11:15:25 -0500 Subject: [PATCH 3/8] Don't delay listen restart on service accept error result. --- src/sessions/session_server.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sessions/session_server.cpp b/src/sessions/session_server.cpp index 28bb75f4c..cbf2fb524 100644 --- a/src/sessions/session_server.cpp +++ b/src/sessions/session_server.cpp @@ -171,13 +171,14 @@ void session_server::handle_accepted(const code& ec, const socket::ptr& socket, return; } - // There was an error accepting the channel, so try again after delay. + // There was an error accepting the channel, so try again. if (ec) { BC_ASSERT_MSG(!socket || socket->stopped(), "unexpected socket"); LOGF("Failed to accept " << (secure ? "private " : "clear ") << name_ << " connection, " << ec.message()); - defer(BIND(start_accept, _1, acceptor, secure)); + ////defer(BIND(start_accept, _1, acceptor, secure)); + start_accept(error::success, acceptor, secure); return; } From 10d05d4782ea87452dd3b960e8184268dd5a9008 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Fri, 6 Feb 2026 11:15:34 -0500 Subject: [PATCH 4/8] Comments. --- src/net/socket_connect.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/net/socket_connect.cpp b/src/net/socket_connect.cpp index 39db7ae7e..3a8930278 100644 --- a/src/net/socket_connect.cpp +++ b/src/net/socket_connect.cpp @@ -194,6 +194,25 @@ void socket::handle_handshake(const boost_code& ec, return; } +//// // Diagnostic block for backend-specific error retrieval. +//// // Boost maps detailed wolfssl errors to a generic error code. +//// if (ec) +//// { +////#ifdef HAVE_SSL +//// const auto handle = std::get(socket_).native_handle(); +//// char buffer[WOLFSSL_MAX_ERROR_SZ]{}; +//// const auto error = ::wolfSSL_get_error(handle, 0); +//// ::wolfSSL_ERR_error_string_n(error, &buffer[0], sizeof(buffer)); +//// LOGF("wolfSSL handshake error: code=" << error << ", desc='" << buffer << "'"); +////#else +//// // OpenSSL recommends 120 bytes for strings. +//// char buffer[120]{}; +//// const auto error = ::ERR_get_error(); +//// ::ERR_error_string_n(error, &buffer[0], sizeof(buffer)); +//// LOGF("OpenSSL handshake error: code=" << error << ", desc='" << buffer << "'"); +////#endif +//// } + const auto code = error::ssl_to_error_code(ec); if (code == error::unknown) logx("handshake", ec); handler(code); From 904c59e090969aae17e819399c35ce020a2e6534 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Fri, 6 Feb 2026 14:48:03 -0500 Subject: [PATCH 5/8] Hack in test condition to discover expected ec. --- test/net/connector.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/net/connector.cpp b/test/net/connector.cpp index 7c30a772d..1a50003fa 100644 --- a/test/net/connector.cpp +++ b/test/net/connector.cpp @@ -201,6 +201,7 @@ BOOST_AUTO_TEST_CASE(connector__connect__stop__resolve_failed_race_operation_can connector::parameters params{ .connect_timeout = seconds(1000), .maximum_request = 42 }; auto instance = std::make_shared(log, strand, pool.service(), suspended, std::move(params)); auto result = true; + code ec1{}; boost::asio::post(strand, [&, instance]()NOEXCEPT { @@ -208,6 +209,7 @@ BOOST_AUTO_TEST_CASE(connector__connect__stop__resolve_failed_race_operation_can instance->connect(config::endpoint{ "bogus.xxx", 42 }, [&](const code& ec, const socket::ptr& socket) NOEXCEPT { + ec1 = ec; result &= (((ec == error::resolve_failed) && !socket) || (ec == error::operation_canceled)); }); @@ -218,6 +220,15 @@ BOOST_AUTO_TEST_CASE(connector__connect__stop__resolve_failed_race_operation_can pool.stop(); BOOST_REQUIRE(pool.join()); BOOST_REQUIRE(instance->get_stopped()); + + // BUGBUG: macOS: + // Error codes have changed and the resulting race failure test is not matching expected code. + if (!result) + { + BOOST_CHECK_EQUAL(ec1, error::resolve_failed); + BOOST_CHECK_EQUAL(ec1, error::operation_canceled); + } + BOOST_REQUIRE(result); } From df02a4af5a46930b2ce3779458fc0416f27e0514 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Fri, 6 Feb 2026 14:48:19 -0500 Subject: [PATCH 6/8] Enable error::unauthorized. --- include/bitcoin/network/error.hpp | 2 +- src/error.cpp | 2 +- test/error.cpp | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/include/bitcoin/network/error.hpp b/include/bitcoin/network/error.hpp index d70aaa451..1fca53f46 100644 --- a/include/bitcoin/network/error.hpp +++ b/include/bitcoin/network/error.hpp @@ -149,7 +149,7 @@ enum error_t : uint8_t // boost beast http 4xx client error bad_request, - ////unauthorized, + unauthorized, ////payment_required, forbidden, not_found, diff --git a/src/error.cpp b/src/error.cpp index 24a41f377..48a5d6bc5 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -136,7 +136,7 @@ DEFINE_ERROR_T_MESSAGE_MAP(error) // boost beast http 4xx client error { bad_request, "bad request" }, - ////{ unauthorized, "unauthorized" }, + { unauthorized, "unauthorized" }, ////{ payment_required, "payment required" }, { forbidden, "forbidden" }, { not_found, "not found" }, diff --git a/test/error.cpp b/test/error.cpp index 075448554..28d8e3e13 100644 --- a/test/error.cpp +++ b/test/error.cpp @@ -769,15 +769,15 @@ BOOST_AUTO_TEST_CASE(error_t__code__bad_request__true_expected_message) BOOST_REQUIRE_EQUAL(ec.message(), "bad request"); } -////BOOST_AUTO_TEST_CASE(error_t__code__unauthorized__true_expected_message) -////{ -//// constexpr auto value = error::unauthorized; -//// const auto ec = code(value); -//// BOOST_REQUIRE(ec); -//// BOOST_REQUIRE(ec == value); -//// BOOST_REQUIRE_EQUAL(ec.message(), "unauthorized"); -////} -//// +BOOST_AUTO_TEST_CASE(error_t__code__unauthorized__true_expected_message) +{ + constexpr auto value = error::unauthorized; + const auto ec = code(value); + BOOST_REQUIRE(ec); + BOOST_REQUIRE(ec == value); + BOOST_REQUIRE_EQUAL(ec.message(), "unauthorized"); +} + ////BOOST_AUTO_TEST_CASE(error_t__code__payment_required__true_expected_message) ////{ //// constexpr auto value = error::payment_required; From b0bf836878d76ec9d1a5335da958ca14f1cd7d3a Mon Sep 17 00:00:00 2001 From: evoskuil Date: Fri, 6 Feb 2026 19:21:19 -0500 Subject: [PATCH 7/8] Add http basic auth settings and computed/cached property. --- include/bitcoin/network/settings.hpp | 9 +++++++++ src/settings.cpp | 13 +++++++++++++ test/settings.cpp | 8 ++++++++ 3 files changed, 30 insertions(+) diff --git a/include/bitcoin/network/settings.hpp b/include/bitcoin/network/settings.hpp index 08270fb72..36ade800e 100644 --- a/include/bitcoin/network/settings.hpp +++ b/include/bitcoin/network/settings.hpp @@ -130,6 +130,15 @@ struct BCT_API settings /// Opaque origins are always serialized as "null". bool allow_opaque_origin{ false }; + /// Basic authorization credential stored and passed in cleartext. + /// This is not security, just compat for bitcoind on secured LAN. + std::string username{}; + std::string password{}; + + /// Requires basic authorization. + virtual bool authorize() const NOEXCEPT; + virtual std::string credential() const NOEXCEPT; + /// Normalized configured hosts/origins helpers. virtual system::string_list host_names() const NOEXCEPT; virtual system::string_list origin_names() const NOEXCEPT; diff --git a/src/settings.cpp b/src/settings.cpp index 5edca2886..6cad0511e 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -142,6 +142,19 @@ system::string_list settings::http_server::origin_names() const NOEXCEPT return config::to_host_names(hosts, port); } +bool settings::http_server::authorize() const NOEXCEPT +{ + return !username.empty() || !password.empty(); +} + +std::string settings::http_server::credential() const NOEXCEPT +{ + static const auto value = "Basic " + + system::encode_base64(username + ":" + password); + + return value; +} + // websocket_server // ---------------------------------------------------------------------------- diff --git a/test/settings.cpp b/test/settings.cpp index a39ed100f..3682f677b 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -404,6 +404,10 @@ BOOST_AUTO_TEST_CASE(settings__http_server__defaults__expected) BOOST_REQUIRE(instance.host_names().empty()); BOOST_REQUIRE(instance.origin_names().empty()); BOOST_REQUIRE(!instance.allow_opaque_origin); + BOOST_REQUIRE(!instance.authorize()); + BOOST_REQUIRE(instance.username.empty()); + BOOST_REQUIRE(instance.password.empty()); + BOOST_REQUIRE_EQUAL(instance.credential(), "Basic Og=="); } BOOST_AUTO_TEST_CASE(settings__websocket_server__defaults__expected) @@ -437,6 +441,10 @@ BOOST_AUTO_TEST_CASE(settings__websocket_server__defaults__expected) BOOST_REQUIRE(instance.host_names().empty()); BOOST_REQUIRE(instance.origin_names().empty()); BOOST_REQUIRE(!instance.allow_opaque_origin); + BOOST_REQUIRE(!instance.authorize()); + BOOST_REQUIRE(instance.username.empty()); + BOOST_REQUIRE(instance.password.empty()); + BOOST_REQUIRE_EQUAL(instance.credential(), "Basic Og=="); // websocket_server (no unique settings yet) } From f6bef77bc80448149da44aa7d3ed2608e0195348 Mon Sep 17 00:00:00 2001 From: evoskuil Date: Fri, 6 Feb 2026 19:21:32 -0500 Subject: [PATCH 8/8] Implement basic auth in channel_http. --- .../bitcoin/network/channels/channel_http.hpp | 8 ++++++ src/channels/channel_http.cpp | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/include/bitcoin/network/channels/channel_http.hpp b/include/bitcoin/network/channels/channel_http.hpp index 8e08e205f..879672be8 100644 --- a/include/bitcoin/network/channels/channel_http.hpp +++ b/include/bitcoin/network/channels/channel_http.hpp @@ -56,6 +56,7 @@ class BCT_API channel_http uint64_t identifier, const settings_t& settings, const options_t& options) NOEXCEPT : channel(log, socket, identifier, settings, options), + options_(options), response_buffer_(system::to_shared()), request_buffer_(options.minimum_buffer) { @@ -79,6 +80,9 @@ class BCT_API channel_http /// Read request buffer (requires strand). virtual http::flat_buffer& request_buffer() NOEXCEPT; + /// Determine if http basic authorization is satisfied if enabled. + virtual bool unauthorized(const http::request& request) NOEXCEPT; + /// Dispatch request to subscribers by verb type. virtual void dispatch(const http::request_cptr& request) NOEXCEPT; @@ -93,11 +97,15 @@ class BCT_API channel_http const result_handler& handler) NOEXCEPT; private: + void handle_unauthorized(const code& ec) NOEXCEPT; void log_message(const http::request& request, size_t bytes) const NOEXCEPT; void log_message(const http::response& response, size_t bytes) const NOEXCEPT; + // This is thread safe. + const options_t& options_; + // These are protected by strand. http::flat_buffer_ptr response_buffer_; http::flat_buffer request_buffer_; diff --git a/src/channels/channel_http.cpp b/src/channels/channel_http.cpp index 73abd20cb..731941f3c 100644 --- a/src/channels/channel_http.cpp +++ b/src/channels/channel_http.cpp @@ -116,6 +116,16 @@ void channel_http::handle_receive(const code& ec, size_t bytes, // Wrap the http request as a tagged verb request and dispatch by type. void channel_http::dispatch(const request_cptr& request) NOEXCEPT { + BC_ASSERT(stranded()); + + if (unauthorized(*request)) + { + send({ status::unauthorized, request->version() }, + std::bind(&channel_http::handle_unauthorized, + shared_from_base(), _1)); + return; + } + rpc::request_t model{}; switch (request.get()->method()) { @@ -185,6 +195,21 @@ void channel_http::assign_json_buffer(response& response) NOEXCEPT } } +// unauthorized helpers +// ---------------------------------------------------------------------------- + +bool channel_http::unauthorized(const http::request& request) NOEXCEPT +{ + return options_.authorize() && + (options_.credential() != request[field::authorization]); +} + +void channel_http::handle_unauthorized(const code& ec) NOEXCEPT +{ + BC_ASSERT(stranded()); + stop(ec ? ec : error::unauthorized); +} + // log helpers // ----------------------------------------------------------------------------