diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp index 615ca7d0..dd7ccef2 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_blockchain.hpp @@ -170,14 +170,6 @@ class BCS_API protocol_bitcoind_blockchain wait_race::ptr race; }; - void arm_wait(waiter&& waiting, double timeout) NOEXCEPT; - void do_wait_event() NOEXCEPT; - void handle_wait_timeout(const code& ec, - const waiter::ptr& waiting) NOEXCEPT; - void complete_wait(const code& ec) NOEXCEPT; - bool wait_done(const waiter& waiting) const NOEXCEPT; - void send_top() NOEXCEPT; - void handle_prioritize(const code& ec, size_t height) NOEXCEPT; void do_precious_block(const code& ec) NOEXCEPT; @@ -188,16 +180,35 @@ class BCS_API protocol_bitcoind_blockchain void do_scan_tx_out_set( const std::shared_ptr& objects, const gate_t::ptr& gate) NOEXCEPT; - void complete_scan(const code& ec, network::rpc::object_t& result, - const gate_t::ptr& gate) NOEXCEPT; + void do_get_descriptor_activity( + const std::shared_ptr& blockhashes, + const std::shared_ptr& scanobjects, + bool include_mempool, const gate_t::ptr& gate) NOEXCEPT; + + void do_get_mempool_entry(const system::hash_digest& hash, + const gate_t::ptr& gate) NOEXCEPT; void do_get_mempool_info(double minimum, double relay, double increment, const gate_t::ptr& gate) NOEXCEPT; bool mempool_entry(network::rpc::object_t& out, const database::tx_link& link) const NOEXCEPT; void do_get_raw_mempool(bool verbose, bool sequence, const gate_t::ptr& gate) NOEXCEPT; - void complete_mempool(const code& ec, network::rpc::value_t& result, + + // Wait machinery. + void arm_wait(waiter&& waiting, double timeout) NOEXCEPT; + void do_wait_event() NOEXCEPT; + void handle_wait_timeout(const code& ec, + const waiter::ptr& waiting) NOEXCEPT; + void complete_wait(const code& ec) NOEXCEPT; + bool wait_done(const waiter& waiting) const NOEXCEPT; + void send_top() NOEXCEPT; + + // Utilities. + void fail(const code& ec, const gate_t::ptr& gate) NOEXCEPT; + void complete_object(network::rpc::object_t& result, + const gate_t::ptr& gate) NOEXCEPT; + void complete_value(network::rpc::value_t& result, const gate_t::ptr& gate) NOEXCEPT; // This is thread safe. diff --git a/include/bitcoin/server/protocols/protocol_rpc.hpp b/include/bitcoin/server/protocols/protocol_rpc.hpp index 018db936..fdadcffc 100644 --- a/include/bitcoin/server/protocols/protocol_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_rpc.hpp @@ -88,11 +88,29 @@ class protocol_rpc if (claimed()) return; - // Silently defer invalidated requests (to the protocol attached last). - if (!is_allowed_host(*post, post->version()) || - !is_allowed_origin(*post, post->version()) || - !post->body().template contains()) + // Enforce http host header (if any hosts are configured). + if (!is_allowed_host(*post, post->version())) + { + set_claimed(); + send_bad_host(*post); + return; + } + + // Enforce http origin policy (if any origins are configured). + if (!is_allowed_origin(*post, post->version())) + { + set_claimed(); + send_forbidden(*post); + return; + } + + // Endpoint accepts only json-rpc posts. + if (!post->body().template contains()) + { + set_claimed(); + send_bad_request(*post); return; + } const auto& message = post->body().template get().message; @@ -129,9 +147,18 @@ class protocol_rpc return; // ws frames carry no headers, so ws authorization is enforced here. - if (!authorized() || - !request.body().template contains()) + if (!authorized()) + { + stop(network::error::unauthorized); return; + } + + // Websocket frames are parsed as json-rpc requests (channel body type). + if (!request.body().template contains()) + { + stop(error::invalid_argument); + return; + } const auto& message = request.body().template get().message; diff --git a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp index 1909252f..caa19f6c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_blockchain.cpp @@ -696,7 +696,6 @@ void protocol_bitcoind_blockchain::do_get_tx_out_set_info(set_hash type, { BC_ASSERT(!stranded()); - object_t result{}; const auto& query = archive(); const auto link = query.to_confirmed(height); @@ -704,8 +703,7 @@ void protocol_bitcoind_blockchain::do_get_tx_out_set_info(set_hash type, database::header_links branch{}; if (!query.get_ancestry(branch, link, height)) { - POST(complete_scan, error::bitcoind::internal_error, - std::move(result), gate); + POST(fail, error::bitcoind::internal_error, gate); return; } @@ -724,20 +722,18 @@ void protocol_bitcoind_blockchain::do_get_tx_out_set_info(set_hash type, if (ec) { - POST(complete_scan, error::bitcoind::internal_error, - std::move(result), gate); + POST(fail, error::bitcoind::internal_error, gate); return; } // A reorganization across the pinned top voids the scan. if (!query.is_confirmed_block(link)) { - POST(complete_scan, error::bitcoind::internal_error, - std::move(result), gate); + POST(fail, error::bitcoind::internal_error, gate); return; } - result = object_t + object_t result { { "height", height }, { "bestblock", encode_hash(query.get_header_key(link)) }, @@ -770,7 +766,7 @@ void protocol_bitcoind_blockchain::do_get_tx_out_set_info(set_hash type, if (type == set_hash::muhash) result.emplace("muhash", encode_hash(digest)); - POST(complete_scan, code{}, std::move(result), gate); + POST(complete_object, std::move(result), gate); } bool protocol_bitcoind_blockchain::handle_prune_block_chain(const code& ec, @@ -827,14 +823,12 @@ void protocol_bitcoind_blockchain::do_scan_tx_out_set( { BC_ASSERT(!stranded()); - object_t result{}; chain::scripts scripts{}; for (const auto& item: *objects) { if (!expand_scan_object(scripts, item, context_)) { - POST(complete_scan, error::bitcoind::invalid_address_or_key, - std::move(result), gate); + POST(fail, error::bitcoind::invalid_address_or_key, gate); return; } } @@ -903,35 +897,19 @@ void protocol_bitcoind_blockchain::do_scan_tx_out_set( if (ec) { - POST(complete_scan, error::bitcoind::internal_error, - std::move(result), gate); + POST(fail, error::bitcoind::internal_error, gate); return; } // A reorganization across the pinned top voids the scan. if (!query.is_confirmed_block(link)) { - POST(complete_scan, error::bitcoind::internal_error, - std::move(result), gate); + POST(fail, error::bitcoind::internal_error, gate); return; } - result = scan_result(coins, query, scripts, top, txouts, bip30, - p2kh_, p2sh_, witness_); - POST(complete_scan, code{}, std::move(result), gate); -} - -void protocol_bitcoind_blockchain::complete_scan(const code& ec, - object_t& result, const gate_t::ptr&) NOEXCEPT -{ - BC_ASSERT(stranded()); - if (stopped()) - return; - - if (ec) - send_error(ec); - else - send_result(std::move(result)); + POST(complete_object, scan_result(coins, query, scripts, top, txouts, bip30, + p2kh_, p2sh_, witness_), gate); } bool protocol_bitcoind_blockchain::handle_verify_chain(const code& ec, @@ -1268,13 +1246,31 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( if (stopped(ec)) return false; + if (include_mempool && !archive().address_enabled()) + { + send_error(error::bitcoind::misc_error); + return true; + } + + PARALLEL(do_get_descriptor_activity, emplace_shared(blockhashes), + emplace_shared(scanobjects), include_mempool, gate()); + return true; +} + +void protocol_bitcoind_blockchain::do_get_descriptor_activity( + const std::shared_ptr& blockhashes, + const std::shared_ptr& scanobjects, bool include_mempool, + const gate_t::ptr& gate) NOEXCEPT +{ + BC_ASSERT(!stranded()); + chain::scripts derived{}; - for (const auto& item: scanobjects) + for (const auto& item: *scanobjects) { if (!expand_scan_object(derived, item, context_)) { - send_error(error::bitcoind::invalid_address_or_key); - return true; + POST(fail, error::bitcoind::invalid_address_or_key, gate); + return; } } @@ -1284,14 +1280,14 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( const auto& query = archive(); array_t activity{}; - for (const auto& item: blockhashes) + for (const auto& item: *blockhashes) { hash_digest hash{}; if (!std::holds_alternative(item.value()) || !decode_hash(hash, std::get(item.value()))) { - send_error(error::bitcoind::invalid_parameter); - return true; + POST(fail, error::bitcoind::invalid_parameter, gate); + return; } constexpr auto witness = true; @@ -1299,22 +1295,22 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( const auto block = query.get_block(link, witness); if (!block) { - send_error(error::bitcoind::invalid_address_or_key); - return true; + POST(fail, error::bitcoind::invalid_address_or_key, gate); + return; } size_t height{}; if (!query.get_height(height, link)) { - send_error(error::bitcoind::internal_error); - return true; + POST(fail, error::bitcoind::internal_error, gate); + return; } // Confirmed spends are unconditional. if (!query.populate_without_metadata(*block)) { - send_error(error::bitcoind::internal_error); - return true; + POST(fail, error::bitcoind::internal_error, gate); + return; } inject_activity(activity, *block, height, encode_hash(hash), watch, @@ -1324,10 +1320,24 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( if (include_mempool) { database::tx_links links{}; - if (query.get_confirmable_txs(links)) + std::unordered_set seen{}; + for (const auto& script: derived) { - send_error(error::bitcoind::internal_error); - return true; + database::histories history{}; + const auto key = sha256_hash(script.to_data(false)); + if (query.get_unconfirmed_history(stopping_, history, key, + max_size_t, database_settings().turbo)) + { + POST(fail, error::bitcoind::internal_error, gate); + return; + } + + for (const auto& item: history) + { + const auto link = query.to_tx(item.tx.hash()); + if (seen.insert(link.value).second) + links.push_back(link.value); + } } for (const auto& link: links) @@ -1335,8 +1345,8 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( const auto tx = query.get_transaction(link, true); if (!tx || !query.populate_without_metadata(*tx)) { - send_error(error::bitcoind::internal_error); - return true; + POST(fail, error::bitcoind::internal_error, gate); + return; } inject_activity(activity, *tx, {}, watch, p2kh_, p2sh_, witness_, @@ -1344,8 +1354,7 @@ bool protocol_bitcoind_blockchain::handle_get_descriptor_activity( } } - send_result(object_t{ { "activity", std::move(activity) } }); - return true; + POST(complete_object, object_t{ { "activity", std::move(activity) } }, gate); } bool protocol_bitcoind_blockchain::handle_get_difficulty(const code& ec, @@ -1598,6 +1607,15 @@ bool protocol_bitcoind_blockchain::handle_get_mempool_entry(const code& ec, return true; } + PARALLEL(do_get_mempool_entry, hash, gate()); + return true; +} + +void protocol_bitcoind_blockchain::do_get_mempool_entry( + const hash_digest& hash, const gate_t::ptr& gate) NOEXCEPT +{ + BC_ASSERT(!stranded()); + const auto& query = archive(); for (const auto& link: query.to_duplicates(hash)) { @@ -1607,16 +1625,15 @@ bool protocol_bitcoind_blockchain::handle_get_mempool_entry(const code& ec, object_t entry{}; if (!mempool_entry(entry, link)) { - send_error(error::bitcoind::internal_error); - return true; + POST(fail, error::bitcoind::internal_error, gate); + return; } - send_result(std::move(entry)); - return true; + POST(complete_value, value_t{ std::move(entry) }, gate); + return; } - send_error(error::bitcoind::invalid_address_or_key); - return true; + POST(fail, error::bitcoind::invalid_address_or_key, gate); } // The minimum is that sent to peers (feefilter), while the relay minimum is @@ -1650,8 +1667,7 @@ void protocol_bitcoind_blockchain::do_get_mempool_info(double minimum, if (!query.get_confirmable_fees(rates) || !database::file::space(space, database_settings().path)) { - POST(complete_mempool, error::bitcoind::internal_error, value_t{}, - gate); + POST(fail, error::bitcoind::internal_error, gate); return; } @@ -1667,22 +1683,23 @@ void protocol_bitcoind_blockchain::do_get_mempool_info(double minimum, const auto usage = ceilinged_add(query.pool_size(), indexes); const auto total_fee = to_floating(fee) / chain::satoshi_per_bitcoin; - object_t result + POST(complete_value, value_t { - { "loaded", true }, - { "size", rates.size() }, - { "bytes", bytes }, - { "usage", usage }, - { "total_fee", total_fee }, - { "maxmempool", space }, - { "mempoolminfee", minimum }, - { "minrelaytxfee", relay }, - { "incrementalrelayfee", increment }, - { "unbroadcastcount", 0 }, - { "fullrbf", false } - }; - - POST(complete_mempool, code{}, value_t{ std::move(result) }, gate); + object_t + { + { "loaded", true }, + { "size", rates.size() }, + { "bytes", bytes }, + { "usage", usage }, + { "total_fee", total_fee }, + { "maxmempool", space }, + { "mempoolminfee", minimum }, + { "minrelaytxfee", relay }, + { "incrementalrelayfee", increment }, + { "unbroadcastcount", 0 }, + { "fullrbf", false } + } + }, gate); } bool protocol_bitcoind_blockchain::handle_get_raw_mempool(const code& ec, @@ -1710,8 +1727,7 @@ void protocol_bitcoind_blockchain::do_get_raw_mempool(bool verbose, const auto& query = archive(); if (query.get_confirmable_txs(links)) { - POST(complete_mempool, error::bitcoind::internal_error, value_t{}, - gate); + POST(fail, error::bitcoind::internal_error, gate); return; } @@ -1723,8 +1739,7 @@ void protocol_bitcoind_blockchain::do_get_raw_mempool(bool verbose, object_t entry{}; if (!mempool_entry(entry, link)) { - POST(complete_mempool, error::bitcoind::internal_error, - value_t{}, gate); + POST(fail, error::bitcoind::internal_error, gate); return; } @@ -1732,7 +1747,7 @@ void protocol_bitcoind_blockchain::do_get_raw_mempool(bool verbose, entries.emplace(txid, std::move(entry)); } - POST(complete_mempool, code{}, value_t{ std::move(entries) }, gate); + POST(complete_value, value_t{ std::move(entries) }, gate); return; } @@ -1743,17 +1758,18 @@ void protocol_bitcoind_blockchain::do_get_raw_mempool(bool verbose, if (!sequence) { - POST(complete_mempool, code{}, value_t{ std::move(txids) }, gate); + POST(complete_value, value_t{ std::move(txids) }, gate); return; } - value_t result = object_t + POST(complete_value, value_t { - { "txids", std::move(txids) }, - { "mempool_sequence", 1 } - }; - - POST(complete_mempool, code{}, std::move(result), gate); + object_t + { + { "txids", std::move(txids) }, + { "mempool_sequence", 1 } + } + }, gate); } // The entry height is that of the chain top, below the pooled context height. @@ -1817,19 +1833,6 @@ bool protocol_bitcoind_blockchain::mempool_entry(object_t& out, return true; } -void protocol_bitcoind_blockchain::complete_mempool(const code& ec, - value_t& result, const gate_t::ptr&) NOEXCEPT -{ - BC_ASSERT(stranded()); - if (stopped()) - return; - - if (ec) - send_error(ec); - else - send_result(std::move(result)); -} - bool protocol_bitcoind_blockchain::handle_get_tx_spending_prevout(const code& ec, rpc_interface::get_tx_spending_prevout) NOEXCEPT { @@ -1965,6 +1968,39 @@ void protocol_bitcoind_blockchain::send_top() NOEXCEPT }); } +// Utilities. +// ---------------------------------------------------------------------------- + +void protocol_bitcoind_blockchain::fail(const code& ec, + const gate_t::ptr&) NOEXCEPT +{ + BC_ASSERT(stranded()); + if (stopped()) + return; + + send_error(ec); +} + +void protocol_bitcoind_blockchain::complete_object(object_t& result, + const gate_t::ptr&) NOEXCEPT +{ + BC_ASSERT(stranded()); + if (stopped()) + return; + + send_result(std::move(result)); +} + +void protocol_bitcoind_blockchain::complete_value(value_t& result, + const gate_t::ptr&) NOEXCEPT +{ + BC_ASSERT(stranded()); + if (stopped()) + return; + + send_result(std::move(result)); +} + BC_POP_WARNING() BC_POP_WARNING() BC_POP_WARNING() diff --git a/src/protocols/native/protocol_native.cpp b/src/protocols/native/protocol_native.cpp index b3da0bbe..eb5a68eb 100644 --- a/src/protocols/native/protocol_native.cpp +++ b/src/protocols/native/protocol_native.cpp @@ -141,8 +141,8 @@ bool protocol_native::try_dispatch_object(const http::request& request) NOEXCEPT if (media == media_type::text_html) return false; - if (const auto ec = dispatcher_.notify(model)) - send_internal_server_error(ec, request); + if (dispatcher_.notify(model)) + send_bad_request(request); return true; } @@ -171,7 +171,10 @@ void protocol_native::dispatch_websocket(const http::request& request) NOEXCEPT rpc::request_t model{}; if (const auto ec = native_target(model, target)) + { + stop(network::error::bad_request); return; + } // Default to json by simulating a json accept header (format overrides). if (!native_query(model, target, { media_type::application_json })) @@ -186,11 +189,8 @@ void protocol_native::dispatch_websocket(const http::request& request) NOEXCEPT return; } - if (const auto ec = dispatcher_.notify(model)) - { - stop(network::error::internal_server_error); - return; - } + if (dispatcher_.notify(model)) + stop(network::error::bad_request); } // Event handlers. diff --git a/test/protocols/electrum/electrum_setup_fixture.cpp b/test/protocols/electrum/electrum_setup_fixture.cpp index 842bba7e..0fa30da6 100644 --- a/test/protocols/electrum/electrum_setup_fixture.cpp +++ b/test/protocols/electrum/electrum_setup_fixture.cpp @@ -152,6 +152,11 @@ bool electrum_setup_fixture::post_handshake(electrum::version version, return verify(post(version_request(version, name, id)), version, id); } +rpc_client::status electrum_setup_fixture::post_status(const std::string& request) +{ + return client_.post_status(request); +} + boost::json::value electrum_setup_fixture::post_authorized( const std::string& request, const std::string& username, const std::string& password) diff --git a/test/protocols/electrum/electrum_setup_fixture.hpp b/test/protocols/electrum/electrum_setup_fixture.hpp index 2456ef2b..c413d4a2 100644 --- a/test/protocols/electrum/electrum_setup_fixture.hpp +++ b/test/protocols/electrum/electrum_setup_fixture.hpp @@ -57,6 +57,9 @@ struct electrum_setup_fixture bool post_handshake(electrum::version version, const std::string& name="test", network::rpc::code_t id={}); + // As post(), returning only the http status. + rpc_client::status post_status(const std::string& request); + // As post(), with basic authorization. boost::json::value post_authorized(const std::string& request, const std::string& username, const std::string& password); @@ -203,6 +206,22 @@ struct electrum_legacy_version_setup_fixture } }; +// Configured with an allowed host that the test client does not send. +struct electrum_hosted_setup_fixture + : electrum_setup_fixture +{ + inline electrum_hosted_setup_fixture() + : electrum_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, true, [](configuration& config) + { + config.server.electrum.hosts = { { "example.com" } }; + }) + { + } +}; + // Configured with a server ping interval (v1.7 websocket and downgraded tcp). struct electrum_ping_setup_fixture : electrum_setup_fixture diff --git a/test/protocols/electrum/electrum_transports.cpp b/test/protocols/electrum/electrum_transports.cpp index 82b95108..4d99678b 100644 --- a/test/protocols/electrum/electrum_transports.cpp +++ b/test/protocols/electrum/electrum_transports.cpp @@ -114,6 +114,23 @@ BOOST_AUTO_TEST_CASE(electrum__ws__scoped_credential_unlisted_method__dropped) REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } +BOOST_AUTO_TEST_CASE(electrum__tcp__credentials_configured__dropped) +{ + const auto response = get(R"({"id":903,"method":"server.version","params":["test","1.4"]})" "\n"); + REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// disallowed host + +BOOST_FIXTURE_TEST_SUITE(electrum_hosted_tests, electrum_hosted_setup_fixture) + +BOOST_AUTO_TEST_CASE(electrum__post__disallowed_host__bad_request) +{ + BOOST_REQUIRE_EQUAL(post_status(R"({"id":904,"method":"server.version","params":["test","1.4"]})"), rpc_client::status::bad_request); +} + BOOST_AUTO_TEST_SUITE_END() // websocket diff --git a/test/protocols/native/native_block.cpp b/test/protocols/native/native_block.cpp index 0c8aa078..4dab4070 100644 --- a/test/protocols/native/native_block.cpp +++ b/test/protocols/native/native_block.cpp @@ -223,6 +223,11 @@ BOOST_AUTO_TEST_CASE(native__dispatch__invalid_stop__bad_request) BOOST_REQUIRE_EQUAL(get_status("/v1/block/subscribe?format=data&stop=maybe"), http::status::bad_request); } +BOOST_AUTO_TEST_CASE(native__dispatch__unsupported_witness__bad_request) +{ + BOOST_REQUIRE_EQUAL(get_status("/v1/top?format=json&witness=false"), http::status::bad_request); +} + BOOST_AUTO_TEST_CASE(native__dispatch__invalid_target__bad_request) { BOOST_REQUIRE_EQUAL(get_status("/v1/bogus"), http::status::bad_request); @@ -239,11 +244,10 @@ BOOST_AUTO_TEST_CASE(native__ws_dispatch__html__error_eof) BOOST_REQUIRE(ws_dropped("/v1/top?format=html")); } -BOOST_AUTO_TEST_CASE(native__ws_dispatch__invalid_target__ignored) +BOOST_AUTO_TEST_CASE(native__ws_dispatch__invalid_target__error_eof) { BOOST_REQUIRE(!ws_upgrade()); - ws_send("/v1/bogus"); - BOOST_REQUIRE_EQUAL(ws_get_text("/v1/top?format=text"), "09"); + BOOST_REQUIRE(ws_dropped("/v1/bogus")); } BOOST_AUTO_TEST_CASE(native__ws_dispatch__unsupported_parameter__error_eof)