diff --git a/include/bitcoin/node/interfaces/bitcoind.hpp b/include/bitcoin/node/interfaces/bitcoind.hpp index e0f9e1bb..cc126fe7 100644 --- a/include/bitcoin/node/interfaces/bitcoind.hpp +++ b/include/bitcoin/node/interfaces/bitcoind.hpp @@ -32,7 +32,7 @@ struct bitcoind_methods { /// Blockchain methods. method<"getbestblockhash">{}, - method<"getblock", string_t, optional<0>>{ "blockhash", "verbosity" }, + method<"getblock", string_t, optional<1>>{ "blockhash", "verbosity" }, method<"getblockchaininfo">{}, method<"getblockcount">{}, method<"getblockfilter", string_t, optional<"basic"_t>>{ "blockhash", "filtertype" }, diff --git a/include/bitcoin/node/protocols/protocol_bitcoind.hpp b/include/bitcoin/node/protocols/protocol_bitcoind.hpp index 3fab06e5..08b8034e 100644 --- a/include/bitcoin/node/protocols/protocol_bitcoind.hpp +++ b/include/bitcoin/node/protocols/protocol_bitcoind.hpp @@ -49,6 +49,9 @@ class BCN_API protocol_bitcoind void start() NOEXCEPT override; protected: + using post = network::http::method::post; + using options = network::http::method::options; + template inline void subscribe(Method&& method, Args&&... args) NOEXCEPT { @@ -56,8 +59,10 @@ class BCN_API protocol_bitcoind } /// Dispatch. + void handle_receive_options(const code& ec, + const network::http::method::options::cptr& options) NOEXCEPT override; void handle_receive_post(const code& ec, - const network::http::method::post::cptr& post) NOEXCEPT override; + const post::cptr& post) NOEXCEPT override; /// Handlers. bool handle_get_best_block_hash(const code& ec, @@ -99,11 +104,19 @@ class BCN_API protocol_bitcoind interface::verify_tx_out_set, const std::string&) NOEXCEPT; private: + // Provide the request for serialization, keeping it out of dispatch. + void set_post(const post::cptr& post) NOEXCEPT; + const post& get_post() const NOEXCEPT; + + // Send the response. + void send_json(boost::json::value&& model, size_t size_hint) NOEXCEPT; + // This is thread safe. ////const options_t& options_; - // This is protected by strand. + // These are protected by strand. dispatcher dispatcher_{}; + post::cptr post_{}; }; } // namespace node diff --git a/include/bitcoin/node/protocols/protocol_html.hpp b/include/bitcoin/node/protocols/protocol_html.hpp index 390ed7da..d3d52914 100644 --- a/include/bitcoin/node/protocols/protocol_html.hpp +++ b/include/bitcoin/node/protocols/protocol_html.hpp @@ -81,8 +81,6 @@ class BCN_API protocol_html const network::http::request& request={}) NOEXCEPT; /// Utilities. - bool is_allowed_origin(const network::http::fields& fields, - size_t version) const NOEXCEPT; std::filesystem::path to_path( const std::string& target = "/") const NOEXCEPT; std::filesystem::path to_local_path( diff --git a/include/bitcoin/node/settings.hpp b/include/bitcoin/node/settings.hpp index f9e39cf3..61a1f115 100644 --- a/include/bitcoin/node/settings.hpp +++ b/include/bitcoin/node/settings.hpp @@ -147,12 +147,6 @@ class BCN_API settings /// Default page for default URL (recommended). std::string default_{ "index.html" }; - /// Validated against origins if configured (recommended). - network::config::endpoints origins{}; - - /// Normalized origins. - virtual system::string_list origin_names() const NOEXCEPT; - /// !path.empty() && http_server::enabled() [hidden, not virtual] virtual bool enabled() const NOEXCEPT; }; diff --git a/src/parser.cpp b/src/parser.cpp index fc547e46..ccd20fdd 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -800,7 +800,12 @@ options_metadata parser::load_settings() THROWS ( "web.origin", value(&configured.server.web.origins), - "The allowed origin (http verification), multiple allowed, defaults to empty (disabled)." + "The allowed origin (see CORS), multiple allowed, defaults to empty (disabled)." + ) + ( + "web.allow_opaque_origin", + value(&configured.server.web.allow_opaque_origin), + "Allow requests from opaue origin (see CORS), multiple allowed, defaults to false." ) ( "web.path", @@ -852,7 +857,12 @@ options_metadata parser::load_settings() THROWS ( "explore.origin", value(&configured.server.explore.origins), - "The allowed origin (http verification), multiple allowed, defaults to empty (disabled)." + "The allowed origin (see CORS), multiple allowed, defaults to empty (disabled)." + ) + ( + "explore.allow_opaque_origin", + value(&configured.server.explore.allow_opaque_origin), + "Allow requests from opaue origin (see CORS), multiple allowed, defaults to false." ) ( "explore.path", @@ -906,6 +916,16 @@ options_metadata parser::load_settings() THROWS value(&configured.server.bitcoind.hosts), "The host name (http verification), multiple allowed, defaults to empty (disabled)." ) + ( + "bitcoind.origin", + value(&configured.server.bitcoind.origins), + "The allowed origin (see CORS), multiple allowed, defaults to empty (disabled)." + ) + ( + "bitcoind.allow_opaque_origin", + value(&configured.server.bitcoind.allow_opaque_origin), + "Allow requests from opaue origin (see CORS), multiple allowed, defaults to false." + ) /* [electrum] */ ////( diff --git a/src/protocols/protocol_bitcoind.cpp b/src/protocols/protocol_bitcoind.cpp index b7cf1609..c7c62c44 100644 --- a/src/protocols/protocol_bitcoind.cpp +++ b/src/protocols/protocol_bitcoind.cpp @@ -34,8 +34,7 @@ using namespace network::http; using namespace std::placeholders; using namespace boost::json; -using json_t = json_body::value_type; - +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) @@ -74,14 +73,55 @@ void protocol_bitcoind::start() NOEXCEPT // Dispatch. // ---------------------------------------------------------------------------- +void protocol_bitcoind::handle_receive_options(const code& ec, + const options::cptr& options) NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (stopped(ec)) + return; + + // Enforce http host header (if any hosts are configured). + if (!is_allowed_host(*options, options->version())) + { + send_bad_host(*options); + return; + } + + // Enforce http origin policy (if any origins are configured). + if (!is_allowed_origin(*options, options->version())) + { + send_forbidden(*options); + return; + } + + send_ok(*options); +} + +// TODO: also handle_receive_get and dispatch based on URL parse. void protocol_bitcoind::handle_receive_post(const code& ec, - const network::http::method::post::cptr& post) NOEXCEPT + const post::cptr& post) NOEXCEPT { BC_ASSERT(stranded()); if (stopped(ec)) return; + // Enforce http host header (if any hosts are configured). + if (!is_allowed_host(*post, post->version())) + { + send_bad_host(*post); + return; + } + + // Enforce http origin policy (if any origins are configured). + if (!is_allowed_origin(*post, post->version())) + { + send_forbidden(*post); + return; + } + + using json_t = json_body::value_type; const auto& body = post->body(); if (!body.contains()) { @@ -105,18 +145,30 @@ void protocol_bitcoind::handle_receive_post(const code& ec, return; } - // TODO: post-process request. + set_post(post); if (const auto code = dispatcher_.notify(request)) stop(code); } // Handlers. // ---------------------------------------------------------------------------- +// github.com/bitcoin/bitcoin/blob/master/doc/JSON-RPC-interface.md +// TODO: precompute size for buffer hints. +// {"jsonrpc": "1.0", "id": "curltest", "method": "getbestblockhash", "params": []} bool protocol_bitcoind::handle_get_best_block_hash(const code& ec, interface::get_best_block_hash) NOEXCEPT { - return !ec; + if (stopped(ec)) + return false; + + const auto& query = archive(); + const auto hash = query.get_header_key(query.to_confirmed( + query.get_top_confirmed())); + + const response_t model{ .result = encode_hash(hash) }; + send_json(value_from(model), two * system::hash_size); + return true; } // method<"getblock", string_t, optional<0_u32>>{ "blockhash", "verbosity" }, @@ -228,6 +280,38 @@ bool protocol_bitcoind::handle_verify_tx_out_set(const code& ec, return !ec; } +// private +// ---------------------------------------------------------------------------- + +void protocol_bitcoind::set_post(const post::cptr& post) NOEXCEPT +{ + BC_ASSERT(post); + post_ = post; +} + +const protocol_bitcoind::post& protocol_bitcoind::get_post() const NOEXCEPT +{ + BC_ASSERT(post_); + return *post_; +} + +// TODO: post-process response for json-rpc version. +void protocol_bitcoind::send_json(boost::json::value&& model, + size_t size_hint) NOEXCEPT +{ + BC_ASSERT(stranded()); + const auto& post = get_post(); + constexpr auto json = media_type::application_json; + response response{ status::ok, post.version() }; + add_common_headers(response, post); + add_access_control_headers(response, post); + response.set(field::content_type, from_media_type(json)); + response.body() = { std::move(model), size_hint }; + response.prepare_payload(); + SEND(std::move(response), handle_complete, _1, error::success); +} + +BC_POP_WARNING() BC_POP_WARNING() BC_POP_WARNING() diff --git a/src/protocols/protocol_explore.cpp b/src/protocols/protocol_explore.cpp index fe1b6701..5f58f93e 100644 --- a/src/protocols/protocol_explore.cpp +++ b/src/protocols/protocol_explore.cpp @@ -934,6 +934,8 @@ bool protocol_explore::handle_get_output_spenders(const code& ec, bool protocol_explore::handle_get_address(const code& ec, interface::address, uint8_t, uint8_t media, const hash_cptr& hash, bool turbo) NOEXCEPT { + BC_ASSERT(stranded()); + if (stopped(ec)) return false; @@ -1010,6 +1012,8 @@ bool protocol_explore::handle_get_address_confirmed(const code& ec, interface::address_confirmed, uint8_t, uint8_t media, const hash_cptr& hash, bool turbo) NOEXCEPT { + BC_ASSERT(stranded()); + if (stopped(ec)) return false; @@ -1045,6 +1049,8 @@ bool protocol_explore::handle_get_address_unconfirmed(const code& ec, interface::address_unconfirmed, uint8_t, uint8_t, const hash_cptr&, bool) NOEXCEPT { + BC_ASSERT(stranded()); + if (stopped(ec)) return false; @@ -1060,6 +1066,8 @@ bool protocol_explore::handle_get_address_balance(const code& ec, interface::address_balance, uint8_t, uint8_t media, const hash_cptr& hash, bool turbo) NOEXCEPT { + BC_ASSERT(stranded()); + if (stopped(ec)) return false; diff --git a/src/protocols/protocol_html.cpp b/src/protocols/protocol_html.cpp index e6fc9ac0..268a26e8 100644 --- a/src/protocols/protocol_html.cpp +++ b/src/protocols/protocol_html.cpp @@ -168,6 +168,7 @@ void protocol_html::send_json(boost::json::value&& model, size_t size_hint, BC_ASSERT(stranded()); response response{ status::ok, request.version() }; add_common_headers(response, request); + add_access_control_headers(response, request); response.set(field::content_type, from_media_type(json)); response.body() = { std::move(model), size_hint }; response.prepare_payload(); @@ -180,6 +181,7 @@ void protocol_html::send_text(std::string&& hexidecimal, BC_ASSERT(stranded()); response response{ status::ok, request.version() }; add_common_headers(response, request); + add_access_control_headers(response, request); response.set(field::content_type, from_media_type(text)); response.body() = std::move(hexidecimal); response.prepare_payload(); @@ -192,6 +194,7 @@ void protocol_html::send_chunk(system::data_chunk&& bytes, BC_ASSERT(stranded()); response response{ status::ok, request.version() }; add_common_headers(response, request); + add_access_control_headers(response, request); response.set(field::content_type, from_media_type(data)); response.body() = std::move(bytes); response.prepare_payload(); @@ -205,6 +208,7 @@ void protocol_html::send_file(file&& file, media_type type, BC_ASSERT_MSG(file.is_open(), "sending closed file handle"); response response{ status::ok, request.version() }; add_common_headers(response, request); + add_access_control_headers(response, request); response.set(field::content_type, from_media_type(type)); response.body() = std::move(file); response.prepare_payload(); @@ -217,6 +221,7 @@ void protocol_html::send_span(span_body::value_type&& span, BC_ASSERT(stranded()); response response{ status::ok, request.version() }; add_common_headers(response, request); + add_access_control_headers(response, request); response.set(field::content_type, from_media_type(type)); response.body() = std::move(span); response.prepare_payload(); @@ -229,6 +234,7 @@ void protocol_html::send_buffer(buffer_body::value_type&& buffer, BC_ASSERT(stranded()); response response{ status::ok, request.version() }; add_common_headers(response, request); + add_access_control_headers(response, request); response.set(field::content_type, from_media_type(type)); response.body() = std::move(buffer); response.prepare_payload(); @@ -238,19 +244,6 @@ void protocol_html::send_buffer(buffer_body::value_type&& buffer, // Utilities. // ---------------------------------------------------------------------------- -bool protocol_html::is_allowed_origin(const fields& fields, - size_t version) const NOEXCEPT -{ - // Allow same-origin and no-origin requests. - // Origin header field is not available until http 1.1. - const auto origin = fields[field::origin]; - if (origin.empty() || version < version_1_1) - return true; - - return options_.origins.empty() || system::contains(options_.origins, - network::config::to_normal_host(origin, default_port())); -} - std::filesystem::path protocol_html::to_path( const std::string& target) const NOEXCEPT { diff --git a/src/settings.cpp b/src/settings.cpp index bef10a88..0d8f03c1 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -180,13 +180,6 @@ settings::html_server::html_server(const std::string_view& logging_name, { } -system::string_list settings::html_server::origin_names() const NOEXCEPT -{ - // secure changes default port from 80 to 443. - const auto port = secure ? http::default_tls : http::default_http; - return network::config::to_host_names(hosts, port); -} - bool settings::html_server::enabled() const NOEXCEPT { return (!path.empty() || pages.enabled()) && http_server::enabled(); diff --git a/test/settings.cpp b/test/settings.cpp index 153427e2..459f1455 100644 --- a/test/settings.cpp +++ b/test/settings.cpp @@ -115,8 +115,6 @@ BOOST_AUTO_TEST_CASE(server__html_server__defaults__expected) BOOST_REQUIRE(instance.websocket); BOOST_REQUIRE(instance.path.empty()); BOOST_REQUIRE_EQUAL(instance.default_, "index.html"); - BOOST_REQUIRE(instance.origins.empty()); - BOOST_REQUIRE(instance.origin_names().empty()); } BOOST_AUTO_TEST_CASE(server__web_server__defaults__expected) @@ -152,8 +150,6 @@ BOOST_AUTO_TEST_CASE(server__web_server__defaults__expected) BOOST_REQUIRE(server.path.empty()); BOOST_REQUIRE(server.websocket); BOOST_REQUIRE_EQUAL(server.default_, "index.html"); - BOOST_REQUIRE(server.origins.empty()); - BOOST_REQUIRE(server.origin_names().empty()); } BOOST_AUTO_TEST_CASE(server__explore_server__defaults__expected) @@ -189,8 +185,6 @@ BOOST_AUTO_TEST_CASE(server__explore_server__defaults__expected) BOOST_REQUIRE(server.path.empty()); BOOST_REQUIRE(server.websocket); BOOST_REQUIRE_EQUAL(server.default_, "index.html"); - BOOST_REQUIRE(server.origins.empty()); - BOOST_REQUIRE(server.origin_names().empty()); } // TODO: could add websocket under bitcoind as a custom property.