diff --git a/builds/cmake/install-cmake.sh b/builds/cmake/install-cmake.sh index bf275606..5b3b0545 100755 --- a/builds/cmake/install-cmake.sh +++ b/builds/cmake/install-cmake.sh @@ -469,21 +469,23 @@ main() UltrafastSecp256k1_FLAGS=() UltrafastSecp256k1_OPTIONS=( - "-DSECP256K1_BUILD_TESTS=OFF" - "-DSECP256K1_BUILD_BENCH=OFF" - "-DSECP256K1_BUILD_EXAMPLES=OFF" - "-DSECP256K1_BUILD_JAVA=OFF" - "-DUFSECP_BUILD_SHARED=OFF" "-DSECP256K1_BUILD_CABI=ON" "-DSECP256K1_BUILD_CPU=ON" "-DSECP256K1_BUILD_SHIM=ON" + "-DSECP256K1_USE_ULTRAFAST=ON" "-DSECP256K1_BUILD_CUDA=ON" "-DSECP256K1_BUILD_ROCM=ON" "-DSECP256K1_BUILD_OPENCL=ON" "-DSECP256K1_BUILD_METAL=ON" + "-DSECP256K1_BUILD_BENCH=OFF" + "-DSECP256K1_BUILD_EXAMPLES=OFF" + "-DSECP256K1_BUILD_JAVA=OFF" + "-DSECP256K1_BUILD_TESTS=OFF" + "-DSECP256K1_SHIM_BUILD_SHARED=OFF" + "-DUFSECP_BUILD_SHARED=OFF" "-DSECP256K1_INSTALL=ON" - "-DSECP256K1_INSTALL_PKGCONFIG=ON" - "-DSECP256K1_USE_ULTRAFAST=ON") + "-DSECP256K1_SHIM_INSTALL=ON" + "-DSECP256K1_INSTALL_PKGCONFIG=ON") libbitcoin_system_FLAGS=() diff --git a/builds/cmake/install-presets.sh b/builds/cmake/install-presets.sh index c3085812..fd8f9b2e 100755 --- a/builds/cmake/install-presets.sh +++ b/builds/cmake/install-presets.sh @@ -485,21 +485,23 @@ main() UltrafastSecp256k1_FLAGS=() UltrafastSecp256k1_OPTIONS=( - "-DSECP256K1_BUILD_TESTS=OFF" - "-DSECP256K1_BUILD_BENCH=OFF" - "-DSECP256K1_BUILD_EXAMPLES=OFF" - "-DSECP256K1_BUILD_JAVA=OFF" - "-DUFSECP_BUILD_SHARED=OFF" "-DSECP256K1_BUILD_CABI=ON" "-DSECP256K1_BUILD_CPU=ON" "-DSECP256K1_BUILD_SHIM=ON" + "-DSECP256K1_USE_ULTRAFAST=ON" "-DSECP256K1_BUILD_CUDA=ON" "-DSECP256K1_BUILD_ROCM=ON" "-DSECP256K1_BUILD_OPENCL=ON" "-DSECP256K1_BUILD_METAL=ON" + "-DSECP256K1_BUILD_BENCH=OFF" + "-DSECP256K1_BUILD_EXAMPLES=OFF" + "-DSECP256K1_BUILD_JAVA=OFF" + "-DSECP256K1_BUILD_TESTS=OFF" + "-DSECP256K1_SHIM_BUILD_SHARED=OFF" + "-DUFSECP_BUILD_SHARED=OFF" "-DSECP256K1_INSTALL=ON" - "-DSECP256K1_INSTALL_PKGCONFIG=ON" - "-DSECP256K1_USE_ULTRAFAST=ON") + "-DSECP256K1_SHIM_INSTALL=ON" + "-DSECP256K1_INSTALL_PKGCONFIG=ON") libbitcoin_system_FLAGS=() diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index f1a06ce1..cee61391 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -52,12 +52,15 @@ src_libbitcoin_server_la_SOURCES = \ ${srcdir}/../../src/parser.cpp \ ${srcdir}/../../src/server_node.cpp \ ${srcdir}/../../src/settings.cpp \ + ${srcdir}/../../src/parsers/admin_query.cpp \ + ${srcdir}/../../src/parsers/admin_target.cpp \ ${srcdir}/../../src/parsers/bitcoind_target.cpp \ ${srcdir}/../../src/parsers/electrum_version.cpp \ ${srcdir}/../../src/parsers/native_query.cpp \ ${srcdir}/../../src/parsers/native_target.cpp \ ${srcdir}/../../src/protocols/protocol_html.cpp \ ${srcdir}/../../src/protocols/protocol_http.cpp \ + ${srcdir}/../../src/protocols/admin/protocol_admin.cpp \ ${srcdir}/../../src/protocols/bitcoind/protocol_bitcoind_rest.cpp \ ${srcdir}/../../src/protocols/bitcoind/protocol_bitcoind_rpc.cpp \ ${srcdir}/../../src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp \ @@ -121,6 +124,7 @@ include_bitcoin_server_interfacesdir = \ ${includedir}/bitcoin/server/interfaces include_bitcoin_server_interfaces_HEADERS = \ + ${srcdir}/../../include/bitcoin/server/interfaces/admin.hpp \ ${srcdir}/../../include/bitcoin/server/interfaces/bitcoind_rest.hpp \ ${srcdir}/../../include/bitcoin/server/interfaces/bitcoind_rpc.hpp \ ${srcdir}/../../include/bitcoin/server/interfaces/btcd.hpp \ @@ -135,6 +139,8 @@ include_bitcoin_server_parsersdir = \ ${includedir}/bitcoin/server/parsers include_bitcoin_server_parsers_HEADERS = \ + ${srcdir}/../../include/bitcoin/server/parsers/admin_query.hpp \ + ${srcdir}/../../include/bitcoin/server/parsers/admin_target.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/bitcoind_target.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/electrum_version.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/native_query.hpp \ @@ -199,10 +205,14 @@ test_libbitcoin_server_test_SOURCES = \ ${srcdir}/../../test/settings.cpp \ ${srcdir}/../../test/test.cpp \ ${srcdir}/../../test/mocks/blocks.cpp \ + ${srcdir}/../../test/parsers/admin_query.cpp \ + ${srcdir}/../../test/parsers/admin_target.cpp \ ${srcdir}/../../test/parsers/bitcoind_target.cpp \ ${srcdir}/../../test/parsers/electrum_version.cpp \ ${srcdir}/../../test/parsers/native_query.cpp \ ${srcdir}/../../test/parsers/native_target.cpp \ + ${srcdir}/../../test/protocols/admin/admin_diagnostics.cpp \ + ${srcdir}/../../test/protocols/admin/admin_setup_fixture.cpp \ ${srcdir}/../../test/protocols/bitcoind/bitcoind_json.cpp \ ${srcdir}/../../test/protocols/bitcoind/bitcoind_rest.cpp \ ${srcdir}/../../test/protocols/bitcoind/bitcoind_rpc.cpp \ diff --git a/builds/gnu/install-gnu.sh b/builds/gnu/install-gnu.sh index 58bbf7a9..9602a409 100755 --- a/builds/gnu/install-gnu.sh +++ b/builds/gnu/install-gnu.sh @@ -497,21 +497,23 @@ main() UltrafastSecp256k1_FLAGS=() UltrafastSecp256k1_OPTIONS=( - "-DSECP256K1_BUILD_TESTS=OFF" - "-DSECP256K1_BUILD_BENCH=OFF" - "-DSECP256K1_BUILD_EXAMPLES=OFF" - "-DSECP256K1_BUILD_JAVA=OFF" - "-DUFSECP_BUILD_SHARED=OFF" "-DSECP256K1_BUILD_CABI=ON" "-DSECP256K1_BUILD_CPU=ON" "-DSECP256K1_BUILD_SHIM=ON" + "-DSECP256K1_USE_ULTRAFAST=ON" "-DSECP256K1_BUILD_CUDA=ON" "-DSECP256K1_BUILD_ROCM=ON" "-DSECP256K1_BUILD_OPENCL=ON" "-DSECP256K1_BUILD_METAL=ON" + "-DSECP256K1_BUILD_BENCH=OFF" + "-DSECP256K1_BUILD_EXAMPLES=OFF" + "-DSECP256K1_BUILD_JAVA=OFF" + "-DSECP256K1_BUILD_TESTS=OFF" + "-DSECP256K1_SHIM_BUILD_SHARED=OFF" + "-DUFSECP_BUILD_SHARED=OFF" "-DSECP256K1_INSTALL=ON" - "-DSECP256K1_INSTALL_PKGCONFIG=ON" - "-DSECP256K1_USE_ULTRAFAST=ON") + "-DSECP256K1_SHIM_INSTALL=ON" + "-DSECP256K1_INSTALL_PKGCONFIG=ON") libbitcoin_system_FLAGS=() diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj index d70fcf34..bf201b9e 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -122,12 +122,16 @@ + + $(IntDir)test_parsers_electrum_version.obj + + @@ -159,6 +163,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters index 329d44ac..ed451ea4 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters @@ -19,15 +19,18 @@ {66A0E586-2E3A-448F-0000-000000000003} - + {66A0E586-2E3A-448F-0000-000000000004} - + {66A0E586-2E3A-448F-0000-000000000005} - + {66A0E586-2E3A-448F-0000-000000000006} + + {66A0E586-2E3A-448F-0000-000000000007} + @@ -42,6 +45,12 @@ src\mocks + + src\parsers + + + src\parsers + src\parsers @@ -54,6 +63,12 @@ src\parsers + + src\protocols\admin + + + src\protocols\admin + src\protocols\bitcoind @@ -137,6 +152,9 @@ src\mocks + + src\protocols\admin + src\protocols\bitcoind diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj index 8f6729ed..be0b878a 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj @@ -124,10 +124,13 @@ + + + @@ -168,6 +171,7 @@ + @@ -178,6 +182,8 @@ + + diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters index 63736a31..ccd3a115 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -49,18 +49,21 @@ {73CE0AC2-ECB2-4E8D-0000-00000000000D} - + {73CE0AC2-ECB2-4E8D-0000-00000000000E} - + {73CE0AC2-ECB2-4E8D-0000-00000000000F} - + {73CE0AC2-ECB2-4E8D-0000-000000000001} - + {73CE0AC2-ECB2-4E8D-0000-000000000002} + + {73CE0AC2-ECB2-4E8D-0000-000000000003} + @@ -72,6 +75,12 @@ src + + src\parsers + + + src\parsers + src\parsers @@ -84,6 +93,9 @@ src\parsers + + src\protocols\admin + src\protocols\bitcoind @@ -200,6 +212,9 @@ include\bitcoin\server + + include\bitcoin\server\interfaces + include\bitcoin\server\interfaces @@ -230,6 +245,12 @@ include\bitcoin\server + + include\bitcoin\server\parsers + + + include\bitcoin\server\parsers + include\bitcoin\server\parsers diff --git a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj index f6e64fd6..f2d54f49 100644 --- a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -122,12 +122,16 @@ + + $(IntDir)test_parsers_electrum_version.obj + + @@ -159,6 +163,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters index 329d44ac..ed451ea4 100644 --- a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters @@ -19,15 +19,18 @@ {66A0E586-2E3A-448F-0000-000000000003} - + {66A0E586-2E3A-448F-0000-000000000004} - + {66A0E586-2E3A-448F-0000-000000000005} - + {66A0E586-2E3A-448F-0000-000000000006} + + {66A0E586-2E3A-448F-0000-000000000007} + @@ -42,6 +45,12 @@ src\mocks + + src\parsers + + + src\parsers + src\parsers @@ -54,6 +63,12 @@ src\parsers + + src\protocols\admin + + + src\protocols\admin + src\protocols\bitcoind @@ -137,6 +152,9 @@ src\mocks + + src\protocols\admin + src\protocols\bitcoind diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj index 2cdd0389..63667347 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj @@ -124,10 +124,13 @@ + + + @@ -168,6 +171,7 @@ + @@ -178,6 +182,8 @@ + + diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters index 63736a31..ccd3a115 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -49,18 +49,21 @@ {73CE0AC2-ECB2-4E8D-0000-00000000000D} - + {73CE0AC2-ECB2-4E8D-0000-00000000000E} - + {73CE0AC2-ECB2-4E8D-0000-00000000000F} - + {73CE0AC2-ECB2-4E8D-0000-000000000001} - + {73CE0AC2-ECB2-4E8D-0000-000000000002} + + {73CE0AC2-ECB2-4E8D-0000-000000000003} + @@ -72,6 +75,12 @@ src + + src\parsers + + + src\parsers + src\parsers @@ -84,6 +93,9 @@ src\parsers + + src\protocols\admin + src\protocols\bitcoind @@ -200,6 +212,9 @@ include\bitcoin\server + + include\bitcoin\server\interfaces + include\bitcoin\server\interfaces @@ -230,6 +245,12 @@ include\bitcoin\server + + include\bitcoin\server\parsers + + + include\bitcoin\server\parsers + include\bitcoin\server\parsers diff --git a/include/bitcoin/server.hpp b/include/bitcoin/server.hpp index 1e1f935f..669f2e2e 100644 --- a/include/bitcoin/server.hpp +++ b/include/bitcoin/server.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/include/bitcoin/server/interfaces/admin.hpp b/include/bitcoin/server/interfaces/admin.hpp new file mode 100644 index 00000000..f24afe2d --- /dev/null +++ b/include/bitcoin/server/interfaces/admin.hpp @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_SERVER_INTERFACES_ADMIN_HPP +#define LIBBITCOIN_SERVER_INTERFACES_ADMIN_HPP + +#include +#include + +namespace libbitcoin { +namespace server { +namespace interface { + +struct admin_methods +{ + static constexpr std::tuple methods + { + method<"log_subscribe", uint8_t, optional<0_u64>>{ "version", "filter" }, + method<"event_subscribe", uint8_t, optional<0_u64>>{ "version", "filter" } + }; + + template + using subscriber = network::unsubscriber; + + template + using at = method_at; + + // Derive this from above in c++26 using reflection. + + using log_subscribe = at<0>; + using event_subscribe = at<1>; +}; + +/// ?format=data|text|json (via query string). +/// --------------------------------------------------------------------------- + +/// The filter parameter is a decimal integer bit mask, where bit position +/// corresponds to log level/event enum value. Zero matches nothing, which +/// clears the subscription; an omitted filter defaults to zero. The response +/// result is the previous filter value, as a json number (masks are +/// constrained to exact json numbers). + +/// /v1/log/subscribe?filter=[mask] {stream} +/// /v1/event/subscribe?filter=[mask] {stream} + +} // namespace interface +} // namespace server +} // namespace libbitcoin + +#endif \ No newline at end of file diff --git a/include/bitcoin/server/interfaces/interfaces.hpp b/include/bitcoin/server/interfaces/interfaces.hpp index 3a82b769..eb805637 100644 --- a/include/bitcoin/server/interfaces/interfaces.hpp +++ b/include/bitcoin/server/interfaces/interfaces.hpp @@ -19,6 +19,7 @@ #ifndef LIBBITCOIN_SERVER_INTERFACES_HPP #define LIBBITCOIN_SERVER_INTERFACES_HPP +#include #include #include #include @@ -31,7 +32,8 @@ namespace libbitcoin { namespace server { namespace interface { - + +using admin = publish; using bitcoind_rest = publish; using bitcoind_rpc = publish; using btcd = publish; diff --git a/include/bitcoin/server/interfaces/native.hpp b/include/bitcoin/server/interfaces/native.hpp index 13f43ca1..a82702e1 100644 --- a/include/bitcoin/server/interfaces/native.hpp +++ b/include/bitcoin/server/interfaces/native.hpp @@ -67,11 +67,7 @@ struct native_methods method<"address_confirmed", uint8_t, uint8_t, system::hash_cptr, optional>{ "version", "media", "hash", "turbo" }, method<"address_unconfirmed", uint8_t, uint8_t, system::hash_cptr, optional>{ "version", "media", "hash", "turbo" }, method<"address_balance", uint8_t, uint8_t, system::hash_cptr, optional>{ "version", "media", "hash", "turbo" }, - method<"address_subscribe", uint8_t, uint8_t, system::hash_cptr, optional, optional>{ "version", "media", "hash", "turbo", "stop" }, - - // TODO: move to admin interface (security). - method<"log_subscribe", uint8_t, uint8_t, optional>{ "version", "media", "stop" }, - method<"event_subscribe", uint8_t, uint8_t, optional>{ "version", "media", "stop" } + method<"address_subscribe", uint8_t, uint8_t, system::hash_cptr, optional, optional>{ "version", "media", "hash", "turbo", "stop" } }; template @@ -120,14 +116,10 @@ struct native_methods using address_unconfirmed = at<29>; using address_balance = at<30>; using address_subscribe = at<31>; - - // TODO: move to admin interface (security). - using log_subscribe = at<32>; - using event_subscribe = at<33>; }; /// ?format=data|text|json (via query string). -/// ----------------------------------------------------------------------- +/// --------------------------------------------------------------------------- /// /v1/configuration {1} @@ -160,20 +152,20 @@ struct native_methods /// /v1/block/hash/[bkhash]/tx/[position] {1} /// /v1/block/height/[height]/tx/[position] {1} -/// ----------------------------------------------------------------------- +/// --------------------------------------------------------------------------- /// /v1/tx/[txhash] {1} /// /v1/tx/[txhash]/header {1 - if confirmed} /// /v1/tx/[txhash]/details {1} -/// ----------------------------------------------------------------------- +/// --------------------------------------------------------------------------- /// /v1/input/[txhash] {all inputs in the tx} /// /v1/input/[txhash]/[index] {1} /// /v1/input/[txhash]/[index]/script {1} /// /v1/input/[txhash]/[index]/witness {1} -/// ----------------------------------------------------------------------- +/// --------------------------------------------------------------------------- /// /v1/output/[txhash] {all outputs in the tx} /// /v1/output/[txhash]/[index] {1} @@ -181,7 +173,7 @@ struct native_methods /// /v1/output/[txhash]/[index]/spender {1 - if confirmed} /// /v1/output/[txhash]/[index]/spenders {all} -/// ----------------------------------------------------------------------- +/// --------------------------------------------------------------------------- /// /v1/address/[output-script-hash] {all} /// /v1/address/[output-script-hash]/unconfirmed {all unconfirmed} diff --git a/include/bitcoin/server/parsers/admin_query.hpp b/include/bitcoin/server/parsers/admin_query.hpp new file mode 100644 index 00000000..2480366f --- /dev/null +++ b/include/bitcoin/server/parsers/admin_query.hpp @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_SERVER_PARSERS_ADMIN_QUERY_HPP +#define LIBBITCOIN_SERVER_PARSERS_ADMIN_QUERY_HPP + +#include + +namespace libbitcoin { +namespace server { + +/// Query string tokens. +namespace admin +{ + namespace token + { + /// Names. + constexpr auto filter = "filter"; + constexpr auto format = "format"; + + /// Format values. + namespace formats + { + constexpr auto html = "html"; + constexpr auto json = "json"; + } + } +} + +// TODO: move into admin namespace. + +BCS_API bool admin_query(network::rpc::request_t& out, + const network::http::request& request) NOEXCEPT; +BCS_API bool admin_query(network::rpc::request_t& out, + const std::string& target, + const network::http::media_types& accepts) NOEXCEPT; + +BCS_API network::http::media_type strip_media( + network::rpc::request_t& model) NOEXCEPT; + +} // namespace server +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/server/parsers/admin_target.hpp b/include/bitcoin/server/parsers/admin_target.hpp new file mode 100644 index 00000000..f1f08a86 --- /dev/null +++ b/include/bitcoin/server/parsers/admin_target.hpp @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_SERVER_PARSERS_ADMIN_TARGET_HPP +#define LIBBITCOIN_SERVER_PARSERS_ADMIN_TARGET_HPP + +#include +#include + +namespace libbitcoin { +namespace server { + +// TODO: move into admin namespace. + +BCS_API code admin_target(network::rpc::request_t& out, + const std::string_view& path) NOEXCEPT; + +} // namespace server +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/server/parsers/bitcoind_target.hpp b/include/bitcoin/server/parsers/bitcoind_target.hpp index ee8b8dbe..6a9255f1 100644 --- a/include/bitcoin/server/parsers/bitcoind_target.hpp +++ b/include/bitcoin/server/parsers/bitcoind_target.hpp @@ -24,6 +24,8 @@ namespace libbitcoin { namespace server { +// TODO: move into bitcoind namespace. + BCS_API code bitcoind_target(network::rpc::request_t& out, const std::string_view& path) NOEXCEPT; diff --git a/include/bitcoin/server/parsers/native_query.hpp b/include/bitcoin/server/parsers/native_query.hpp index 2197696e..3b0f5882 100644 --- a/include/bitcoin/server/parsers/native_query.hpp +++ b/include/bitcoin/server/parsers/native_query.hpp @@ -24,29 +24,34 @@ namespace libbitcoin { namespace server { -// Query string tokens. -namespace token +/// Query string tokens. +namespace native { - // Names. - constexpr auto stop = "stop"; - constexpr auto turbo = "turbo"; - constexpr auto format = "format"; - constexpr auto witness = "witness"; - - // Boolean values. - constexpr auto true_ = "true"; - constexpr auto false_ = "false"; - - // Format values. - namespace formats + namespace token { - constexpr auto html = "html"; - constexpr auto text = "text"; - constexpr auto json = "json"; - constexpr auto data = "data"; + /// Names. + constexpr auto stop = "stop"; + constexpr auto turbo = "turbo"; + constexpr auto format = "format"; + constexpr auto witness = "witness"; + + /// Boolean values. + constexpr auto true_ = "true"; + constexpr auto false_ = "false"; + + /// Format values. + namespace formats + { + constexpr auto html = "html"; + constexpr auto text = "text"; + constexpr auto json = "json"; + constexpr auto data = "data"; + } } } +// TODO: move into native namespace. + BCS_API bool native_query(network::rpc::request_t& out, const network::http::request& request) NOEXCEPT; BCS_API bool native_query(network::rpc::request_t& out, diff --git a/include/bitcoin/server/parsers/parsers.hpp b/include/bitcoin/server/parsers/parsers.hpp index 04c1de08..b60a4308 100644 --- a/include/bitcoin/server/parsers/parsers.hpp +++ b/include/bitcoin/server/parsers/parsers.hpp @@ -19,6 +19,8 @@ #ifndef LIBBITCOIN_SERVER_PARSERS_PARSERS_HPP #define LIBBITCOIN_SERVER_PARSERS_PARSERS_HPP +#include +#include #include #include #include diff --git a/include/bitcoin/server/protocols/protocol_admin.hpp b/include/bitcoin/server/protocols/protocol_admin.hpp index 18197027..53d7a37e 100644 --- a/include/bitcoin/server/protocols/protocol_admin.hpp +++ b/include/bitcoin/server/protocols/protocol_admin.hpp @@ -16,23 +16,28 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -#ifndef LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_WEB_HPP -#define LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_WEB_HPP +#ifndef LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_ADMIN_HPP +#define LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_ADMIN_HPP +#include #include +#include +#include #include +#include #include namespace libbitcoin { namespace server { -/// Administrative admin site for the node (currently just page server). class BCS_API protocol_admin : public protocol_html, protected network::tracker { public: typedef std::shared_ptr ptr; + using interface = server::interface::admin; + using dispatcher = network::rpc::dispatcher; inline protocol_admin(const auto& session, const network::channel::ptr& channel, @@ -41,6 +46,61 @@ class BCS_API protocol_admin network::tracker(session->log) { } + + void start() NOEXCEPT override; + void stopping(const code& ec) NOEXCEPT override; + +protected: + template + inline void subscribe(Method&& method, Args&&... args) NOEXCEPT + { + dispatcher_.subscribe(BIND_SHARED(method, args)); + } + + /// Dispatch. + bool try_dispatch_object( + const network::http::request& request) NOEXCEPT override; + void dispatch_websocket( + const network::http::request& request) NOEXCEPT override; + + /// Event handlers. + /// ----------------------------------------------------------------------- + + bool handle_log(const code& ec, uint8_t level, time_t zulu, + const std::string& message) NOEXCEPT; + bool handle_event(const code& ec, uint8_t event_, uint64_t value, + const network::logger::time& point) NOEXCEPT; + + /// Interface handlers. + /// ----------------------------------------------------------------------- + + bool handle_get_log_subscribe(const code& ec, interface::log_subscribe, + uint8_t version, uint64_t filter) NOEXCEPT; + bool handle_get_event_subscribe(const code& ec, interface::event_subscribe, + uint8_t version, uint64_t filter) NOEXCEPT; + +protected: + /// Notification event handlers (protocol strand). + /// ----------------------------------------------------------------------- + void do_log(uint8_t level, time_t zulu, const std::string& message) NOEXCEPT; + void do_event(uint8_t event_, time_t zulu, uint64_t value) NOEXCEPT; + +private: + using filter_t = std::atomic; + + // Utils. + static time_t to_zulu(const network::logger::time& point) NOEXCEPT; + static bool get_filter(bool& filtered, size_t bit, + filter_t& filter) NOEXCEPT; + static bool update_filter(uint64_t& prior, uint64_t value, + filter_t& filter) NOEXCEPT; + + // These are thread safe. + filter_t log_state_{}; + filter_t event_state_{}; + + // This is protected by strand. + dispatcher dispatcher_{}; }; } // namespace server diff --git a/include/bitcoin/server/protocols/protocol_native.hpp b/include/bitcoin/server/protocols/protocol_native.hpp index 00d74b16..e9fa4bf1 100644 --- a/include/bitcoin/server/protocols/protocol_native.hpp +++ b/include/bitcoin/server/protocols/protocol_native.hpp @@ -70,10 +70,6 @@ class BCS_API protocol_native bool handle_chase(const code&, node::chase event_, node::event_value) NOEXCEPT; - bool handle_log(const code& ec, uint8_t level, time_t time, - const std::string& message) NOEXCEPT; - bool handle_events(const code& ec, uint8_t event_, uint64_t value, - const network::logger::time& point) NOEXCEPT; /// Interface handlers. /// ----------------------------------------------------------------------- @@ -180,12 +176,6 @@ class BCS_API protocol_native interface::address_subscribe, uint8_t version, uint8_t media, const system::hash_cptr& hash, bool turbo, bool stop) NOEXCEPT; - // TODO: move to admin interface. - bool handle_get_log_subscribe(const code& ec, interface::log_subscribe, - uint8_t version, uint8_t media, bool stop) NOEXCEPT; - bool handle_get_event_subscribe(const code& ec, interface::event_subscribe, - uint8_t version, uint8_t media, bool stop) NOEXCEPT; - protected: using media_type = network::http::media_type; @@ -277,12 +267,6 @@ class BCS_API protocol_native // TODO: map of scripthashes (notify on instances). std::atomic_bool address_subscribe_{}; - // TODO: map of log levels. - std::atomic_bool log_subscribe_{}; - - // TODO: map of events. - std::atomic_bool event_subscribe_{}; - // This is protected by strand. dispatcher dispatcher_{}; }; diff --git a/src/parsers/admin_query.cpp b/src/parsers/admin_query.cpp new file mode 100644 index 00000000..c3f9332c --- /dev/null +++ b/src/parsers/admin_query.cpp @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include + +#include + +namespace libbitcoin { +namespace server { + +using namespace system; +using namespace network; +using namespace network::http; + +BC_PUSH_WARNING(NO_ARRAY_INDEXING) +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +// Masks are constrained to exact json number (ieee double) representation. +constexpr auto maximum_filter = sub1(power2(53u)); + +template +static bool to_number(Number& out, const std::string& token) NOEXCEPT +{ + return !token.empty() && is_ascii_numeric(token) && (is_one(token.size()) || + token.front() != '0') && deserialize(out, token); +} + +inline void set_media(rpc::object_t& params, media_type media) NOEXCEPT +{ + params["media"] = to_value(media); +} + +bool admin_query(rpc::request_t& out, const request& request) NOEXCEPT +{ + const auto accepts = to_media_types((request)[field::accept]); + return admin_query(out, request.target(), accepts); +} + +bool admin_query(rpc::request_t& out, const std::string& target, + const media_types& accepts) NOEXCEPT +{ + wallet::uri uri{}; + if (!uri.decode(target)) + return false; + + constexpr auto html = media_type::text_html; + constexpr auto json = media_type::application_json; + + // Caller must have provided a request.params object. + if (!out.params.has_value() || + !std::holds_alternative(out.params.value())) + return false; + + using namespace server::admin; + auto query = uri.decode_query(); + auto& params = std::get(out.params.value()); + + // Filter is required by admin methods but not html (page) requests. + // Omission for dispatched (non-html) requests is rejected by dispatch. + if (const auto filter = query.find(token::filter); filter != query.end()) + { + uint64_t value{}; + if (!to_number(value, filter->second) || value > maximum_filter) + return false; + + params[token::filter] = value; + } + + const auto format = query[token::format]; + + // Prioritize query string format over http headers. + if (format == token::formats::json) + set_media(params, json); + else if (format == token::formats::html) + set_media(params, html); + else if (!format.empty()) + return false; + + // Priotize: json, html (ignores accept priorities). + else if (contains(accepts, json)) + set_media(params, json); + else if (contains(accepts, html)) + set_media(params, html); + //else no media type is set, which results in not acceptable. + + // Parse successful, media type not acceptable if not set. + return true; +} + +media_type strip_media(rpc::request_t& model) NOEXCEPT +{ + if (model.params.has_value()) + { + auto& params = std::get(model.params.value()); + const auto& media = params.find("media"); + if (media != params.end() && std::holds_alternative( + media->second.value())) + { + switch (const auto value = static_cast( + std::get(media->second.value()))) + { + case media_type::text_html: + case media_type::application_json: + { + params.erase("media"); + return value; + } + default: + return media_type::unknown; + } + } + } + + return media_type::unknown; +} + +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/parsers/admin_target.cpp b/src/parsers/admin_target.cpp new file mode 100644 index 00000000..dc394e17 --- /dev/null +++ b/src/parsers/admin_target.cpp @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include + +#include + +namespace libbitcoin { +namespace server { + +using namespace system; +using namespace network::rpc; + +BC_PUSH_WARNING(NO_ARRAY_INDEXING) +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +template +static bool to_number(Number& out, const std::string_view& token) NOEXCEPT +{ + return !token.empty() && is_ascii_numeric(token) && (is_one(token.size()) || + token.front() != '0') && deserialize(out, token); +} + +code admin_target(request_t& out, const std::string_view& path) NOEXCEPT +{ + const auto clean = split(path, "?", false, false).front(); + if (clean.empty()) + return error::empty_path; + + // Avoid conflict with node type. + using object_t = network::rpc::object_t; + + // Initialize json-rpc.v2 named params message. + out = request_t + { + .jsonrpc = version::v2, + .id = null_t{}, + .method = {}, + .params = object_t{} + }; + + auto& method = out.method; + auto& params = std::get(out.params.value()); + const auto segments = split(clean, "/", false, true); + BC_ASSERT(!segments.empty()); + + size_t segment{}; + if (!segments[segment].starts_with('v')) + return error::missing_version; + + uint8_t version{}; + if (!to_number(version, segments[segment++].substr(one))) + return error::invalid_number; + + params["version"] = version; + if (segment == segments.size()) + return error::missing_target; + + const auto target = segments[segment++]; + if (target == "log") + { + if (segment == segments.size()) + return error::invalid_subcomponent; + + if (segments[segment++] == "subscribe") + method = "log_subscribe"; + else + return error::invalid_subcomponent; + } + else if (target == "event") + { + if (segment == segments.size()) + return error::invalid_subcomponent; + + if (segments[segment++] == "subscribe") + method = "event_subscribe"; + else + return error::invalid_subcomponent; + } + else + { + return error::invalid_target; + } + + return segment == segments.size() ? error::success : error::extra_segment; +} + +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/parsers/native_query.cpp b/src/parsers/native_query.cpp index c6717987..8cdc1054 100644 --- a/src/parsers/native_query.cpp +++ b/src/parsers/native_query.cpp @@ -32,12 +32,12 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) inline bool is_true(const std::string& value) NOEXCEPT { - return value == token::true_; + return value == native::token::true_; } inline bool is_false(const std::string& value) NOEXCEPT { - return value == token::false_; + return value == native::token::false_; } inline void set_media(rpc::object_t& params, media_type media) NOEXCEPT @@ -68,6 +68,7 @@ bool native_query(rpc::request_t& out, const std::string& target, !std::holds_alternative(out.params.value())) return false; + using namespace server::native; auto query = uri.decode_query(); auto& params = std::get(out.params.value()); @@ -121,10 +122,9 @@ bool native_query(rpc::request_t& out, const std::string& target, set_media(params, text); else if (contains(accepts, data)) set_media(params, data); - else - return false; + //else no media type is set, which results in not acceptable. - // Media type has been set. + // Parse successful, media type not acceptable if not set. return true; } diff --git a/src/protocols/admin/protocol_admin.cpp b/src/protocols/admin/protocol_admin.cpp new file mode 100644 index 00000000..bd6ac350 --- /dev/null +++ b/src/protocols/admin/protocol_admin.cpp @@ -0,0 +1,291 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include + +#include +#include +#include +#include +#include + +namespace libbitcoin { +namespace server { + +using namespace system; +using namespace network; +using namespace network::http; +using namespace std::placeholders; +constexpr auto relaxed = std::memory_order_relaxed; + +#define CLASS protocol_admin +#define SUBSCRIBE_ADMIN(method, ...) \ + subscribe(&CLASS::method, __VA_ARGS__) + +// Limited by json number domain and sentinel bit. +static_assert(node::events::unknown < 53u); +static_assert(network::levels::verbose < 53u); + +// Start. +// ---------------------------------------------------------------------------- + +void protocol_admin::start() NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (started()) + return; + + // Subscription methods. + SUBSCRIBE_ADMIN(handle_get_log_subscribe, _1, _2, _3, _4); + SUBSCRIBE_ADMIN(handle_get_event_subscribe, _1, _2, _3, _4); + protocol_html::start(); +} + +void protocol_admin::stopping(const code& ec) NOEXCEPT +{ + BC_ASSERT(stranded()); + dispatcher_.stop(ec); + protocol_html::stopping(ec); +} + +// Dispatch. +// ---------------------------------------------------------------------------- + +bool protocol_admin::try_dispatch_object(const http::request& request) NOEXCEPT +{ + BC_ASSERT(stranded()); + + BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + const auto target = request.target(); + BC_POP_WARNING() + + rpc::request_t model{}; + if (const auto ec = admin_target(model, target)) + { + // Allow invalid interface target to be retried as a page request. + return !ec; + } + + // No media defaults injected for an http request. + if (!admin_query(model, request)) + { + send_bad_request(request); + return true; + } + + const auto media = strip_media(model); + if (media == media_type::unknown) + { + send_not_acceptable(request); + return true; + } + + // Falls through to html page dispatch. + if (media == media_type::text_html) + return false; + + if (const auto ec = dispatcher_.notify(model)) + send_internal_server_error(ec, request); + + return true; +} + +void protocol_admin::dispatch_websocket(const http::request& request) NOEXCEPT +{ + BC_ASSERT(stranded()); + + // Admin websocket interface supports only string requests. + if (!request.body().contains()) + { + stop(network::error::not_acceptable); + return; + } + + // Target with query string is passed via websocket string body. + const auto target = request.body().get(); + + rpc::request_t model{}; + if (const auto ec = admin_target(model, target)) + return; + + // Default to json by simulating a json accept header (format overrides). + if (!admin_query(model, target, { media_type::application_json })) + { + stop(network::error::bad_request); + return; + } + + if (strip_media(model) != media_type::application_json) + { + stop(network::error::not_acceptable); + return; + } + + if (const auto ec = dispatcher_.notify(model)) + { + stop(network::error::internal_server_error); + return; + } +} + +// Interface handlers. +// ---------------------------------------------------------------------------- + +bool protocol_admin::handle_get_log_subscribe(const code& ec, + interface::log_subscribe, uint8_t /*version*/, uint64_t filter) NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (stopped(ec)) + return false; + + // log outlives node, so cannot hold shared_ptr to the protocol. + uint64_t previous{}; + if (update_filter(previous, filter, log_state_)) + log.subscribe_messages(BIND_WEAK(handle_log, _1, _2, _3, _4)); + + send_json({ { "previous", previous } }, 42); + return true; +} + +bool protocol_admin::handle_get_event_subscribe(const code& ec, + interface::event_subscribe, uint8_t /*version*/, uint64_t filter) NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (stopped(ec)) + return false; + + // log outlives node, so cannot hold shared_ptr to the protocol. + uint64_t previous{}; + if (update_filter(previous, filter, event_state_)) + log.subscribe_events(BIND_WEAK(handle_event, _1, _2, _3, _4)); + + send_json({ { "previous", previous } }, 42); + return true; +} + +// Event handlers. +// ---------------------------------------------------------------------------- + +bool protocol_admin::handle_log(const code& ec, uint8_t level, time_t zulu, + const std::string& message) NOEXCEPT +{ + bool filtered{}; + if (stopped(ec) || !get_filter(filtered, level, log_state_)) + return false; + + if (!filtered || !websocket()) + return true; + + POST(do_log, level, zulu, message); + return true; +} + +bool protocol_admin::handle_event(const code& ec, uint8_t event_, + uint64_t value, const logger::time& point) NOEXCEPT +{ + bool filtered{}; + if (stopped(ec) || !get_filter(filtered, event_, event_state_)) + return false; + + if (!filtered || !websocket()) + return true; + + POST(do_event, event_, to_zulu(point), value); + return true; +} + +// Notification event handlers. +// ---------------------------------------------------------------------------- +// Posted to the protocol strand. + +void protocol_admin::do_log(uint8_t level, time_t zulu, + const std::string& message) NOEXCEPT +{ + BC_ASSERT(stranded() && websocket()); + + if (stopped()) + return; + + notify_json( + { + { "level", level }, + { "time", zulu }, + { "message", message } + }, message.size() + 64u); +} + +void protocol_admin::do_event(uint8_t event_, time_t zulu, + uint64_t value_) NOEXCEPT +{ + BC_ASSERT(stranded() && websocket()); + + if (stopped()) + return; + + notify_json( + { + { "event", event_ }, + { "value", value_ }, + { "time", zulu } + }, 64); +} + +// Utils. +// ---------------------------------------------------------------------------- +// static/private + +time_t protocol_admin::to_zulu(const logger::time& point) NOEXCEPT +{ + using namespace std::chrono; + const auto now = logger::now(); + const auto delta = duration_cast(now - point); + return system_clock::to_time_t(system_clock::now() - delta); +} + +// Unsubscribe if false. +bool protocol_admin::get_filter(bool& filtered, size_t bit, + filter_t& filter) NOEXCEPT +{ + auto state = filter.load(relaxed); + const auto next = set_left(state, zero, false); + + // Empty filter unregisters, failure implies a new filter arrived. + if (is_zero(next) && filter.compare_exchange_strong(state, next, relaxed)) + return false; + + filtered = get_right(state, bit); + return true; +} + +// Subscribe if true. +bool protocol_admin::update_filter(uint64_t& prior, uint64_t value, + filter_t& filter) NOEXCEPT +{ + const auto next = set_left(value, zero, true); + const auto state = is_zero(value) ? filter.fetch_and(next, relaxed) : + filter.exchange(next, relaxed); + + prior = set_left(state, zero, false); + return !is_zero(value) && !get_left(state); +} + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/native/protocol_native.cpp b/src/protocols/native/protocol_native.cpp index 29dd3868..bf396fba 100644 --- a/src/protocols/native/protocol_native.cpp +++ b/src/protocols/native/protocol_native.cpp @@ -93,10 +93,6 @@ void protocol_native::start() NOEXCEPT SUBSCRIBE_NATIVE(handle_get_address_unconfirmed, _1, _2, _3, _4, _5, _6); SUBSCRIBE_NATIVE(handle_get_address_balance, _1, _2, _3, _4, _5, _6); SUBSCRIBE_NATIVE(handle_get_address_subscribe, _1, _2, _3, _4, _5, _6, _7); - - // Admin endpoint methods (TODO: move to admin interface). - SUBSCRIBE_NATIVE(handle_get_log_subscribe, _1, _2, _3, _4, _5); - SUBSCRIBE_NATIVE(handle_get_event_subscribe, _1, _2, _3, _4, _5); protocol_html::start(); } @@ -122,15 +118,27 @@ bool protocol_native::try_dispatch_object(const http::request& request) NOEXCEPT rpc::request_t model{}; if (const auto ec = native_target(model, target)) + { + // Allow invalid interface target to be retried as a page request. return !ec; + } + // No media defaults injected for an http request. if (!native_query(model, request)) + { + send_bad_request(request); + return true; + } + + const auto media = get_media(model); + if (media == media_type::unknown) { send_not_acceptable(request); return true; } - if (get_media(model) == media_type::text_html) + // Falls through to html page dispatch. + if (media == media_type::text_html) return false; if (const auto ec = dispatcher_.notify(model)) @@ -158,8 +166,13 @@ void protocol_native::dispatch_websocket(const http::request& request) NOEXCEPT return; // Default to json by simulating a json accept header (format overrides). - if (!native_query(model, target, { media_type::application_json }) || - get_media(model) == media_type::text_html) + if (!native_query(model, target, { media_type::application_json })) + { + stop(network::error::bad_request); + return; + } + + if (get_media(model) == media_type::text_html) { stop(network::error::not_acceptable); return; @@ -239,32 +252,6 @@ bool protocol_native::handle_chase(const code&, node::chase event_, return true; } -bool protocol_native::handle_log(const code& ec, uint8_t , time_t , - const std::string& ) NOEXCEPT -{ - if (stopped(ec) || !log_subscribe_.load(relaxed)) - return false; - - if (!websocket()) - return true; - - // TODO: map subscribed flags to event filter, emit single json event. - return false; -} - -bool protocol_native::handle_events(const code& ec, uint8_t , - uint64_t , const logger::time& ) NOEXCEPT -{ - if (stopped(ec) || !event_subscribe_.load(relaxed)) - return false; - - if (!websocket()) - return true; - - // TODO: map subscribed flags to event filter, emit single json event. - return false; -} - // Utilities. // ---------------------------------------------------------------------------- // private diff --git a/src/protocols/native/protocol_native_configuration.cpp b/src/protocols/native/protocol_native_configuration.cpp index 9fa2d3ab..634d0499 100644 --- a/src/protocols/native/protocol_native_configuration.cpp +++ b/src/protocols/native/protocol_native_configuration.cpp @@ -29,6 +29,8 @@ using namespace std::placeholders; #define CLASS protocol_native +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + bool protocol_native::handle_get_configuration(const code& ec, interface::configuration, uint8_t, uint8_t media) NOEXCEPT { @@ -41,8 +43,7 @@ bool protocol_native::handle_get_configuration(const code& ec, return true; } - BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) - boost::json::object object + send_json(boost::json::object { { "address", archive().address_enabled() }, { "filter", archive().filter_enabled() }, @@ -51,54 +52,7 @@ bool protocol_native::handle_get_configuration(const code& ec, { "retarget", system_settings().forks.retarget }, { "difficult", system_settings().forks.difficult }, { "stripped", get_stripped_height() }, - }; - BC_POP_WARNING() - - send_json(std::move(object), 64); - return true; -} - -bool protocol_native::handle_get_log_subscribe(const code& ec, - interface::log_subscribe, uint8_t , uint8_t , - bool stop) NOEXCEPT -{ - if (stopped(ec)) - return false; - - // TODO: add levels param, if empty implies stop. - // TODO: implement as bit flags that map to levels here. - log_subscribe_.store(stop, std::memory_order_relaxed); - if (stop) - { - send_empty(); - return true; - } - - // handle_log will unsubscribe when !log_subscribe_ upon any message. - log.subscribe_messages(BIND(handle_log, _1, _2, _3, _4)); - send_empty(); - return true; -} - -bool protocol_native::handle_get_event_subscribe(const code& ec, - interface::event_subscribe, uint8_t , uint8_t , - bool stop) NOEXCEPT -{ - if (stopped(ec)) - return false; - - // TODO: add events param, if empty implies stop. - // TODO: implement as bit flags that map to events here. - event_subscribe_.store(stop, std::memory_order_relaxed); - if (stop) - { - send_empty(); - return true; - } - - // handle_events will unsubscribe when !log_subscribe_ upon any event. - log.subscribe_events(BIND(handle_events, _1, _2, _3, _4)); - send_empty(); + }, 64); return true; } @@ -137,5 +91,7 @@ size_t protocol_native::get_active_height( query.is_candidate_header(link) ? ctx.height : zero; } +BC_POP_WARNING() + } // namespace server } // namespace libbitcoin diff --git a/test/mocks/blocks.cpp b/test/mocks/blocks.cpp index 5f753584..62167f37 100644 --- a/test/mocks/blocks.cpp +++ b/test/mocks/blocks.cpp @@ -82,6 +82,18 @@ const block block9{ block9_data, true }; const server::settings::embedded_pages web_pages{}; +boost::json::value parse_json(std::string_view value) NOEXCEPT +{ + try + { + return boost::json::parse(value); + } + catch (...) + { + return {}; + } +} + bool setup_ten_block_store(query_t& query) NOEXCEPT { return query.initialize(genesis) && diff --git a/test/mocks/blocks.hpp b/test/mocks/blocks.hpp index 164fe0f0..f158d2a9 100644 --- a/test/mocks/blocks.hpp +++ b/test/mocks/blocks.hpp @@ -104,6 +104,24 @@ bool setup_three_block_witness_store(query_t& query) NOEXCEPT; bool setup_three_block_confirmed_address_store(query_t& query) NOEXCEPT; bool setup_three_block_unconfirmed_address_store(query_t& query) NOEXCEPT; +// TODO: move. +boost::json::value parse_json(std::string_view value) NOEXCEPT; + } // namespace test +namespace libbitcoin { +namespace network { +namespace http { + +// TODO: move. +inline std::ostream& operator<<(std::ostream& os, const media_type& media) +{ + os << from_media_type(media); + return os; +} + +} // namespace http +} // namespace network +} // namespace libbitcoin + #endif diff --git a/test/parsers/admin_query.cpp b/test/parsers/admin_query.cpp new file mode 100644 index 00000000..b21a755f --- /dev/null +++ b/test/parsers/admin_query.cpp @@ -0,0 +1,324 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../test.hpp" +#include "../mocks/blocks.hpp" + +BOOST_AUTO_TEST_SUITE(admin_query_tests) + +using namespace network::rpc; +using namespace network::http; + +// parse failures + +BOOST_AUTO_TEST_CASE(parsers__admin_query__empty__false) +{ + request_t out{}; + BOOST_REQUIRE(!admin_query(out, {})); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__no_params_object__false) +{ + request_t out{}; + BOOST_REQUIRE(!admin_query(out, "/", {})); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__invalid_uri_target__false) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(!admin_query(out, "invalid uri with spaces?foo=bar", {})); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__no_supported_media__unknown) +{ + request_t out{}; + out.params = object_t{}; + const media_types accepts{ media_type::text_plain }; + BOOST_REQUIRE(admin_query(out, "/", accepts)); + BOOST_REQUIRE_EQUAL(strip_media(out), media_type::unknown); +} + +// strip_media + +BOOST_AUTO_TEST_CASE(parsers__admin_query__no_query_no_accepts__unknown) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(admin_query(out, "/", {})); + BOOST_REQUIRE_EQUAL(strip_media(out), media_type::unknown); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__no_query_accept_json__json) +{ + request_t out{}; + out.params = object_t{}; + const media_types accepts{ media_type::application_json }; + BOOST_REQUIRE(admin_query(out, "/", accepts)); + BOOST_REQUIRE_EQUAL(strip_media(out), media_type::application_json); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__no_query_accept_html__html) +{ + request_t out{}; + out.params = object_t{}; + const media_types accepts{ media_type::text_html }; + BOOST_REQUIRE(admin_query(out, "/", accepts)); + BOOST_REQUIRE_EQUAL(strip_media(out), media_type::text_html); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__no_query_accept_priority_json__json) +{ + request_t out{}; + out.params = object_t{}; + const media_types accepts{ media_type::text_html, media_type::application_json }; + BOOST_REQUIRE(admin_query(out, "/", accepts)); + BOOST_REQUIRE_EQUAL(strip_media(out), media_type::application_json); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__query_format_json__json) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(admin_query(out, "/?format=json", {})); + BOOST_REQUIRE_EQUAL(strip_media(out), media_type::application_json); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__query_format_html__html) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(admin_query(out, "/?format=html", {})); + BOOST_REQUIRE_EQUAL(strip_media(out), media_type::text_html); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__query_format_text__false) +{ + // Text is not a supported admin format. + request_t out{}; + out.params = object_t{}; + const media_types accepts{ media_type::application_json }; + BOOST_REQUIRE(!admin_query(out, "/?format=text", accepts)); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__query_format_data__false) +{ + // Data is not a supported admin format. + request_t out{}; + out.params = object_t{}; + const media_types accepts{ media_type::application_json }; + BOOST_REQUIRE(!admin_query(out, "/?format=data", accepts)); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__query_format_unknown__false) +{ + request_t out{}; + out.params = object_t{}; + const media_types accepts{ media_type::application_json }; + BOOST_REQUIRE(!admin_query(out, "/?format=foo", accepts)); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__query_format_overrides_accept__true) +{ + request_t out{}; + out.params = object_t{}; + const media_types accepts{ media_type::application_json }; + BOOST_REQUIRE(admin_query(out, "/?format=html", accepts)); + BOOST_REQUIRE_EQUAL(strip_media(out), media_type::text_html); +} + +// filter + +BOOST_AUTO_TEST_CASE(parsers__admin_query__filter_omitted__no_param) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(admin_query(out, "/?format=json", {})); + + const auto& params = std::get(out.params.value()); + BOOST_REQUIRE(params.find(admin::token::filter) == params.end()); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__filter_zero__expected) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(admin_query(out, "/?filter=0&format=json", {})); + + const auto& params = std::get(out.params.value()); + const auto it = params.find(admin::token::filter); + BOOST_REQUIRE(it != params.end()); + BOOST_REQUIRE(std::holds_alternative(it->second.value())); + BOOST_REQUIRE_EQUAL(std::get(it->second.value()), 0u); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__filter_value__expected) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(admin_query(out, "/?filter=123&format=json", {})); + + const auto& params = std::get(out.params.value()); + const auto it = params.find(admin::token::filter); + BOOST_REQUIRE(it != params.end()); + BOOST_REQUIRE(std::holds_alternative(it->second.value())); + BOOST_REQUIRE_EQUAL(std::get(it->second.value()), 123u); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__filter_maximum__expected) +{ + // 2^53 - 1 is the maximum exact json number. + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(admin_query(out, "/?filter=9007199254740991&format=json", {})); + + const auto& params = std::get(out.params.value()); + const auto it = params.find(admin::token::filter); + BOOST_REQUIRE(it != params.end()); + BOOST_REQUIRE_EQUAL(std::get(it->second.value()), 9007199254740991u); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__filter_excess__false) +{ + // 2^53 exceeds the maximum exact json number. + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(!admin_query(out, "/?filter=9007199254740992&format=json", {})); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__filter_non_numeric__false) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(!admin_query(out, "/?filter=abc&format=json", {})); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__filter_leading_zero__false) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(!admin_query(out, "/?filter=01&format=json", {})); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__filter_empty__false) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(!admin_query(out, "/?filter=&format=json", {})); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__filter_negative__false) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(!admin_query(out, "/?filter=-1&format=json", {})); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_query__multiple_params__true) +{ + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(admin_query(out, "/?filter=7&format=json", {})); + BOOST_REQUIRE_EQUAL(strip_media(out), media_type::application_json); + + const auto& params = std::get(out.params.value()); + BOOST_REQUIRE(params.find(admin::token::filter) != params.end()); +} + +// http request overload + +BOOST_AUTO_TEST_CASE(parsers__admin_query__http_request_overload_with_accept_json__json) +{ + network::http::request request{}; + request.target("/?filter=1"); + request.set(field::accept, "application/json"); + + request_t out{}; + out.params = object_t{}; + BOOST_REQUIRE(admin_query(out, request)); + BOOST_REQUIRE_EQUAL(strip_media(out), media_type::application_json); +} + +// strip_media + +BOOST_AUTO_TEST_CASE(parsers__strip_media__no_params__unknown) +{ + request_t model{}; + BOOST_REQUIRE_EQUAL(strip_media(model), media_type::unknown); +} + +BOOST_AUTO_TEST_CASE(parsers__strip_media__no_media_key__unknown) +{ + request_t model{}; + model.params = object_t{}; + BOOST_REQUIRE_EQUAL(strip_media(model), media_type::unknown); +} + +BOOST_AUTO_TEST_CASE(parsers__strip_media__invalid_type__unknown) +{ + request_t model{}; + model.params = object_t{}; + auto& params = std::get(model.params.value()); + params["media"] = value_t{ true }; // not uint8_t + BOOST_REQUIRE_EQUAL(strip_media(model), media_type::unknown); +} + +BOOST_AUTO_TEST_CASE(parsers__strip_media__json__json_and_erased) +{ + request_t model{}; + model.params = object_t{}; + auto& params = std::get(model.params.value()); + params["media"] = value_t{ static_cast(media_type::application_json) }; + BOOST_REQUIRE_EQUAL(strip_media(model), media_type::application_json); + + // The media param is stripped from the model (undeclared in interface). + BOOST_REQUIRE(params.find("media") == params.end()); + BOOST_REQUIRE_EQUAL(strip_media(model), media_type::unknown); +} + +BOOST_AUTO_TEST_CASE(parsers__strip_media__html__html_and_erased) +{ + request_t model{}; + model.params = object_t{}; + auto& params = std::get(model.params.value()); + params["media"] = value_t{ static_cast(media_type::text_html) }; + BOOST_REQUIRE_EQUAL(strip_media(model), media_type::text_html); + BOOST_REQUIRE(params.find("media") == params.end()); +} + +BOOST_AUTO_TEST_CASE(parsers__strip_media__unsupported_value__unknown_not_erased) +{ + // Unsupported media is not stripped (dispatch rejection preserved). + request_t model{}; + model.params = object_t{}; + auto& params = std::get(model.params.value()); + params["media"] = value_t{ static_cast(media_type::text_plain) }; + BOOST_REQUIRE_EQUAL(strip_media(model), media_type::unknown); + BOOST_REQUIRE(params.find("media") != params.end()); +} + +BOOST_AUTO_TEST_CASE(parsers__strip_media__unknown_value__unknown) +{ + request_t model{}; + model.params = object_t{}; + auto& params = std::get(model.params.value()); + params["media"] = value_t{ 99_u8 }; + BOOST_REQUIRE_EQUAL(strip_media(model), media_type::unknown); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/parsers/admin_target.cpp b/test/parsers/admin_target.cpp new file mode 100644 index 00000000..f4124bf2 --- /dev/null +++ b/test/parsers/admin_target.cpp @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../test.hpp" + +BOOST_AUTO_TEST_SUITE(admin_target_tests) + +using namespace system; +using namespace network::rpc; +using object_t = network::rpc::object_t; + +// General errors + +BOOST_AUTO_TEST_CASE(parsers__admin_target__empty_path__empty_path) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "?foo=bar"), server::error::empty_path); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__missing_version__missing_version) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/"), server::error::missing_version); + BOOST_REQUIRE_EQUAL(admin_target(out, "/log/subscribe"), server::error::missing_version); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__invalid_version__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/vinvalid/log/subscribe"), server::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__version_leading_zero__invalid_number) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/v01/log/subscribe"), server::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__missing_target__missing_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3"), server::error::missing_target); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__invalid_target__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3/invalid"), server::error::invalid_target); +} + +// log/subscribe + +BOOST_AUTO_TEST_CASE(parsers__admin_target__log_subscribe_valid__expected) +{ + const std::string path = "/v42/log/subscribe"; + + request_t request{}; + BOOST_REQUIRE(!admin_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "log_subscribe"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 1u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__log_subscribe_sloppy_valid__expected) +{ + const std::string path = "//v255//log//subscribe//?foo=bar"; + + request_t request{}; + BOOST_REQUIRE(!admin_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "log_subscribe"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 1u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__log_missing_subcomponent__invalid_subcomponent) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3/log"), server::error::invalid_subcomponent); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__log_invalid_subcomponent__invalid_subcomponent) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3/log/invalid"), server::error::invalid_subcomponent); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__log_subscribe_extra_segment__extra_segment) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3/log/subscribe/extra"), server::error::extra_segment); +} + +// event/subscribe + +BOOST_AUTO_TEST_CASE(parsers__admin_target__event_subscribe_valid__expected) +{ + const std::string path = "/v42/event/subscribe"; + + request_t request{}; + BOOST_REQUIRE(!admin_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "event_subscribe"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 1u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__event_missing_subcomponent__invalid_subcomponent) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3/event"), server::error::invalid_subcomponent); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__event_invalid_subcomponent__invalid_subcomponent) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3/event/invalid"), server::error::invalid_subcomponent); +} + +BOOST_AUTO_TEST_CASE(parsers__admin_target__event_subscribe_extra_segment__extra_segment) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3/event/subscribe/extra"), server::error::extra_segment); +} + +// Cross-interface targets (native grammar is not admin grammar). + +BOOST_AUTO_TEST_CASE(parsers__admin_target__native_target__invalid_target) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3/top"), server::error::invalid_target); + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3/configuration"), server::error::invalid_target); + BOOST_REQUIRE_EQUAL(admin_target(out, "/v3/block/height/123"), server::error::invalid_target); +} + +BOOST_AUTO_TEST_SUITE_END() + diff --git a/test/parsers/native_query.cpp b/test/parsers/native_query.cpp index a633a4b6..0007b92d 100644 --- a/test/parsers/native_query.cpp +++ b/test/parsers/native_query.cpp @@ -17,20 +17,7 @@ * along with this program. If not, see . */ #include "../test.hpp" - -namespace libbitcoin { -namespace network { -namespace http { - -std::ostream& operator<<(std::ostream& os, const media_type& media) -{ - os << from_media_type(media); - return os; -} - -} // namespace http -} // namespace network -} // namespace libbitcoin +#include "../mocks/blocks.hpp" BOOST_AUTO_TEST_SUITE(native_query_tests) @@ -56,11 +43,13 @@ BOOST_AUTO_TEST_CASE(parsers__native_query__invalid_uri_target__false) BOOST_REQUIRE(!native_query(out, "invalid uri with spaces?foo=bar", {})); } -BOOST_AUTO_TEST_CASE(parsers__native_query__no_query_no_accepts__false) +// valid parse doesn't require acceptable. +BOOST_AUTO_TEST_CASE(parsers__native_query__no_query_no_accepts__true) { request_t out{}; out.params = object_t{}; - BOOST_REQUIRE(!native_query(out, "/", {})); + BOOST_REQUIRE(native_query(out, "/", {})); + BOOST_REQUIRE_EQUAL(get_media(out), media_type::unknown); } BOOST_AUTO_TEST_CASE(parsers__native_query__no_query_accept_json__true) @@ -135,12 +124,14 @@ BOOST_AUTO_TEST_CASE(parsers__native_query__no_query_accept_priority_data__true) BOOST_REQUIRE_EQUAL(get_media(out), media_type::application_octet_stream); } -BOOST_AUTO_TEST_CASE(parsers__native_query__no_query_no_matching_accept__false) +// valid parse doesn't require acceptable. +BOOST_AUTO_TEST_CASE(parsers__native_query__no_query_no_matching_accept__true) { request_t out{}; out.params = object_t{}; const media_types accepts{ media_type::unknown }; - BOOST_REQUIRE(!native_query(out, "/", accepts)); + BOOST_REQUIRE(native_query(out, "/", accepts)); + BOOST_REQUIRE_EQUAL(get_media(out), media_type::unknown); } BOOST_AUTO_TEST_CASE(parsers__native_query__query_format_json__true) @@ -208,7 +199,7 @@ BOOST_AUTO_TEST_CASE(parsers__native_query__witness_false__true) BOOST_REQUIRE(native_query(out, "/?witness=false", accepts)); const auto& params = std::get(out.params.value()); - const auto it = params.find(token::witness); + const auto it = params.find(native::token::witness); BOOST_REQUIRE(it != params.end()); BOOST_REQUIRE(std::holds_alternative(it->second.value())); BOOST_REQUIRE(!std::get(it->second.value())); @@ -222,7 +213,7 @@ BOOST_AUTO_TEST_CASE(parsers__native_query__witness_true__true) BOOST_REQUIRE(native_query(out, "/?witness=true", accepts)); const auto& params = std::get(out.params.value()); - BOOST_REQUIRE(params.find(token::witness) == params.end()); + BOOST_REQUIRE(params.find(native::token::witness) == params.end()); } BOOST_AUTO_TEST_CASE(parsers__native_query__turbo_false__true) @@ -233,7 +224,7 @@ BOOST_AUTO_TEST_CASE(parsers__native_query__turbo_false__true) BOOST_REQUIRE(native_query(out, "/?turbo=false", accepts)); const auto& params = std::get(out.params.value()); - const auto it = params.find(token::turbo); + const auto it = params.find(native::token::turbo); BOOST_REQUIRE(it != params.end()); BOOST_REQUIRE(std::holds_alternative(it->second.value())); BOOST_REQUIRE(!std::get(it->second.value())); @@ -247,7 +238,7 @@ BOOST_AUTO_TEST_CASE(parsers__native_query__turbo_true__true) BOOST_REQUIRE(native_query(out, "/?turbo=true", accepts)); const auto& params = std::get(out.params.value()); - BOOST_REQUIRE(params.find(token::turbo) == params.end()); + BOOST_REQUIRE(params.find(native::token::turbo) == params.end()); } BOOST_AUTO_TEST_CASE(parsers__native_query__stop_true__true) @@ -258,7 +249,7 @@ BOOST_AUTO_TEST_CASE(parsers__native_query__stop_true__true) BOOST_REQUIRE(native_query(out, "/?stop=true", accepts)); const auto& params = std::get(out.params.value()); - const auto it = params.find(token::stop); + const auto it = params.find(native::token::stop); BOOST_REQUIRE(it != params.end()); BOOST_REQUIRE(std::holds_alternative(it->second.value())); BOOST_REQUIRE(std::get(it->second.value())); @@ -272,7 +263,7 @@ BOOST_AUTO_TEST_CASE(parsers__native_query__stop_false__true) BOOST_REQUIRE(native_query(out, "/?stop=false", accepts)); const auto& params = std::get(out.params.value()); - BOOST_REQUIRE(params.find(token::stop) == params.end()); + BOOST_REQUIRE(params.find(native::token::stop) == params.end()); } BOOST_AUTO_TEST_CASE(parsers__native_query__stop_invalid__false) diff --git a/test/protocols/admin/admin_diagnostics.cpp b/test/protocols/admin/admin_diagnostics.cpp new file mode 100644 index 00000000..9d40b548 --- /dev/null +++ b/test/protocols/admin/admin_diagnostics.cpp @@ -0,0 +1,251 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../../test.hpp" +#include "admin_setup_fixture.hpp" + +using namespace network::http; + +BOOST_FIXTURE_TEST_SUITE(admin_tests, admin_ten_block_setup_fixture) + +// log subscribe (http) +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(admin__log_subscribe__json__previous_zero) +{ + const auto response = get_json("/v1/log/subscribe?filter=1&format=json"); + REQUIRE_NO_THROW_TRUE(response.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("previous").as_int64(), 0); +} + +BOOST_AUTO_TEST_CASE(admin__log_subscribe__omitted_filter__previous_zero) +{ + const auto response = get_json("/v1/log/subscribe?format=json"); + REQUIRE_NO_THROW_TRUE(response.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("previous").as_int64(), 0); +} + +BOOST_AUTO_TEST_CASE(admin__log_subscribe__repeat__previous_expected) +{ + auto response = get_json("/v1/log/subscribe?filter=3&format=json"); + REQUIRE_NO_THROW_TRUE(response.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("previous").as_int64(), 0); + + response = get_json("/v1/log/subscribe?filter=5&format=json"); + REQUIRE_NO_THROW_TRUE(response.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("previous").as_int64(), 3); + + response = get_json("/v1/log/subscribe?filter=0&format=json"); + REQUIRE_NO_THROW_TRUE(response.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("previous").as_int64(), 5); + + response = get_json("/v1/log/subscribe?filter=0&format=json"); + REQUIRE_NO_THROW_TRUE(response.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("previous").as_int64(), 0); +} + +BOOST_AUTO_TEST_CASE(admin__log_subscribe__maximum_filter__previous_zero) +{ + // 2^53 - 1 is the maximum exact json number. + const auto response = get_json("/v1/log/subscribe?filter=9007199254740991&format=json"); + REQUIRE_NO_THROW_TRUE(response.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("previous").as_int64(), 0); +} + +BOOST_AUTO_TEST_CASE(admin__log_subscribe__excess_filter__bad_request) +{ + // 2^53 exceeds the maximum exact json number. + const auto value = get_status("/v1/log/subscribe?filter=9007199254740992&format=json"); + BOOST_REQUIRE(value == status::bad_request); +} + +BOOST_AUTO_TEST_CASE(admin__log_subscribe__non_numeric_filter__bad_request) +{ + const auto value = get_status("/v1/log/subscribe?filter=abc&format=json"); + BOOST_REQUIRE(value == status::bad_request); +} + +BOOST_AUTO_TEST_CASE(admin__log_subscribe__leading_zero_filter__bad_request) +{ + const auto value = get_status("/v1/log/subscribe?filter=01&format=json"); + BOOST_REQUIRE(value == status::bad_request); +} + +BOOST_AUTO_TEST_CASE(admin__log_subscribe__xml__bad_request) +{ + // An unrecognized format value is malformed, not a negotiation failure. + const auto value = get_status("/v1/log/subscribe?filter=1&format=xml"); + BOOST_REQUIRE(value == status::bad_request); +} + +BOOST_AUTO_TEST_CASE(admin__log_subscribe__no_format__not_acceptable) +{ + const auto value = get_status("/v1/log/subscribe?filter=1"); + BOOST_REQUIRE(value == status::not_acceptable); +} + +// event subscribe (http) +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(admin__event_subscribe__json__previous_zero) +{ + const auto response = get_json("/v1/event/subscribe?filter=1&format=json"); + REQUIRE_NO_THROW_TRUE(response.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(response.at("previous").as_int64(), 0); +} + +// subscribe (websockets) +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(admin__ws_log_subscribe__json__previous_zero) +{ + BOOST_REQUIRE(!ws_upgrade()); + + const auto ack = ws_get_json("/v1/log/subscribe?filter=1024"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 0); +} + +BOOST_AUTO_TEST_CASE(admin__ws_subscribe__channels__independent) +{ + BOOST_REQUIRE(!ws_upgrade()); + + // Masks use bits above defined levels/events (no live traffic). + auto ack = ws_get_json("/v1/log/subscribe?filter=1024"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 0); + + ack = ws_get_json("/v1/event/subscribe?filter=2048"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 0); + + ack = ws_get_json("/v1/log/subscribe?filter=0"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 1024); + + ack = ws_get_json("/v1/event/subscribe?filter=0"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 2048); +} + +BOOST_AUTO_TEST_CASE(admin__ws_log_subscribe__invalid_filter__error_eof) +{ + BOOST_REQUIRE(!ws_upgrade()); + BOOST_REQUIRE(ws_dropped("/v1/log/subscribe?filter=xyz")); +} + +BOOST_AUTO_TEST_CASE(admin__ws_log_subscribe__html__error_eof) +{ + BOOST_REQUIRE(!ws_upgrade()); + BOOST_REQUIRE(ws_dropped("/v1/log/subscribe?filter=1&format=html")); +} + +// streams (websockets) +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(admin__ws_log_subscribe__write__notified) +{ + BOOST_REQUIRE(!ws_upgrade()); + + // Bit 10 is above defined levels; the server's own request logging + // (generated by this test's traffic) can never match the mask. + const auto ack = ws_get_json("/v1/log/subscribe?filter=1024"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 0); + + write(10, "test"); + + const auto frame = ws_receive_json(); + REQUIRE_NO_THROW_TRUE(frame.at("level").is_int64()); + REQUIRE_NO_THROW_TRUE(frame.at("time").is_int64()); + REQUIRE_NO_THROW_TRUE(frame.at("message").is_string()); + BOOST_REQUIRE_EQUAL(frame.at("level").as_int64(), 10); + BOOST_REQUIRE_EQUAL(frame.at("message").as_string(), "test"); +} + +BOOST_AUTO_TEST_CASE(admin__ws_log_subscribe__unfiltered_level__not_notified) +{ + BOOST_REQUIRE(!ws_upgrade()); + + const auto ack = ws_get_json("/v1/log/subscribe?filter=1024"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 0); + + // Sequential commitment on the logger strand orders these writes. + write(11, "dropped"); + write(10, "matched"); + + const auto frame = ws_receive_json(); + REQUIRE_NO_THROW_TRUE(frame.at("level").is_int64()); + BOOST_REQUIRE_EQUAL(frame.at("level").as_int64(), 10); + REQUIRE_NO_THROW_TRUE(frame.at("message").is_string()); + BOOST_REQUIRE_EQUAL(frame.at("message").as_string(), "matched"); +} + +BOOST_AUTO_TEST_CASE(admin__ws_log_subscribe__live_level__eventually_notified) +{ + BOOST_REQUIRE(!ws_upgrade()); + + const auto ack = ws_get_json("/v1/log/subscribe?filter=1"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 0); + + write(0, "sentinel"); + + // Server self-logging may interleave; drain to the sentinel. + while (ws_receive_json().at("message").as_string() != "sentinel"); +} + +BOOST_AUTO_TEST_CASE(admin__ws_event_subscribe__fire__notified) +{ + BOOST_REQUIRE(!ws_upgrade()); + + // block_archived (bit 3), no node activity fires events in this test. + const auto ack = ws_get_json("/v1/event/subscribe?filter=8"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 0); + + fire(node::events::block_archived, 42); + + const auto frame = ws_receive_json(); + REQUIRE_NO_THROW_TRUE(frame.at("time").is_int64()); + REQUIRE_NO_THROW_TRUE(frame.at("event").is_int64()); + REQUIRE_NO_THROW_TRUE(frame.at("value").is_int64()); + BOOST_REQUIRE_EQUAL(frame.at("event").as_int64(), node::events::block_archived); + BOOST_REQUIRE_EQUAL(frame.at("value").as_int64(), 42); +} + +BOOST_AUTO_TEST_CASE(admin__ws_event_subscribe__unfiltered_event__not_notified) +{ + BOOST_REQUIRE(!ws_upgrade()); + + const auto ack = ws_get_json("/v1/event/subscribe?filter=8"); + REQUIRE_NO_THROW_TRUE(ack.at("previous").is_int64()); + BOOST_REQUIRE_EQUAL(ack.at("previous").as_int64(), 0); + + fire(node::events::header_archived, 1); + fire(node::events::block_archived, 2); + + const auto frame = ws_receive_json(); + REQUIRE_NO_THROW_TRUE(frame.at("event").is_int64()); + REQUIRE_NO_THROW_TRUE(frame.at("value").is_int64()); + BOOST_REQUIRE_EQUAL(frame.at("event").as_int64(), node::events::block_archived); + BOOST_REQUIRE_EQUAL(frame.at("value").as_int64(), 2); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/admin/admin_setup_fixture.cpp b/test/protocols/admin/admin_setup_fixture.cpp new file mode 100644 index 00000000..92af5ede --- /dev/null +++ b/test/protocols/admin/admin_setup_fixture.cpp @@ -0,0 +1,275 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../../test.hpp" +#include "../../mocks/blocks.hpp" +#include "admin_setup_fixture.hpp" +#include + +using namespace system; +using namespace boost::beast; + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +admin_setup_fixture::admin_setup_fixture(const initializer& setup) + : config_ + { + system::chain::selection::mainnet, + test::web_pages, + test::web_pages + }, + store_ + { + [&]() NOEXCEPT -> const database::settings& + { + config_.database.path = TEST_DIRECTORY; + return config_.database; + }() + }, + query_{ store_ }, log_{}, + server_{ query_, config_, log_ } +{ + test::clear(test::directory); + auto& database_settings = config_.database; + auto& network_settings = config_.network; + auto& node_settings = config_.node; + auto& server_settings = config_.server; + auto& admin = server_settings.admin; + + admin.binds = { { ADMIN_ENDPOINT } }; + admin.connections = 1; + admin.path = "unused"; + admin.inactivity_minutes = 1; + database_settings.interval_depth = 2; + node_settings.delay_inbound = false; + node_settings.minimum_fee_rate = 99.0; + network_settings.inbound.connections = 0; + network_settings.outbound.connections = 0; + + // Create and populate the store. + auto ec = store_.create([](auto, auto) {}); + BOOST_REQUIRE_MESSAGE(!ec, ec.message()); + setup(query_); + + // Run the server. + std::promise running{}; + server_.run([&](const code& ec) NOEXCEPT + { + running.set_value(ec); + }); + + // Block until server is running. + ec = running.get_future().get(); + BOOST_REQUIRE_MESSAGE(!ec, ec.message()); + socket_.connect(admin.binds.back().to_endpoint()); +} + +admin_setup_fixture::~admin_setup_fixture() +{ + network::boost_code ec{}; + if (websocket_.has_value()) + { + websocket_.value().close(websocket::close_code::normal, ec); + + // Expected and harmless during fixture teardown: + // beast::websocket::error::closed : normal (graceful handshake). + // asio::error::operation_aborted : hard (invalid request test). + if (ec && + ec != boost::beast::websocket::error::closed && + ec != boost::asio::error::operation_aborted) + { + BOOST_WARN_MESSAGE(false, ec.message()); + } + } + else + { + socket_.close(); + } + + server_.close(); + ec = store_.close([](auto, auto){}); + BOOST_WARN_MESSAGE(!ec, ec.message()); + test::clear(test::directory); +} + +BC_POP_WARNING() + +admin_setup_fixture::string_request +admin_setup_fixture::create_request(std::string_view target) +{ + // Build HTTP/1.1 GET string request. + string_request request{ http::verb::get, target, network::http::version_1_1 }; + request.set(http::field::host, "localhost"); + request.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + request.keep_alive(true); + return request; +} + +bool admin_setup_fixture::expect_dropped(std::string_view target) +{ + http::write(socket_, create_request(target)); + + flat_buffer buffer{}; + network::boost_code ec{}; + http::response response{}; + http::read(socket_, buffer, response, ec); + return ec == boost::beast::net::error::eof; +} + +http::status admin_setup_fixture::get_status(std::string_view target) +{ + http::write(socket_, create_request(target)); + + flat_buffer buffer{}; + network::boost_code ec{}; + http::response response{}; + http::read(socket_, buffer, response, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + + return response.result(); +} + +std::string admin_setup_fixture::get_text(std::string_view target) +{ + http::write(socket_, create_request(target)); + + flat_buffer buffer{}; + network::boost_code ec{}; + http::response response{}; + http::read(socket_, buffer, response, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_EQUAL(response.result(), http::status::ok); + + return response.body(); +} + +system::data_chunk admin_setup_fixture::get_data(std::string_view target) +{ + http::write(socket_, create_request(target)); + + flat_buffer buffer{}; + network::boost_code ec{}; + http::response response{}; + http::read(socket_, buffer, response, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_EQUAL(response.result(), http::status::ok); + + return system::data_chunk(response.body().begin(), response.body().end()); +} + +// The network json body does not support reading a document consisting +// of only a top-level primitive, because it supports streaming and non- +// streaming. So instead just use a string buffer and parse explicitly. +boost::json::value admin_setup_fixture::get_json(std::string_view target) +{ + http::write(socket_, create_request(target)); + + flat_buffer buffer{}; + network::boost_code ec{}; + http::response response{}; + http::read(socket_, buffer, response, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + BOOST_CHECK_EQUAL(response.result(), http::status::ok); + + return test::parse_json(response.body()); +} + +network::boost_code admin_setup_fixture::ws_upgrade() +{ + network::boost_code ec{}; + BOOST_CHECK(!websocket_.has_value()); + + websocket_.emplace(socket_); + websocket_.value().text(true); + websocket_.value().handshake("localhost", "/", ec); + return ec; +} + +data_chunk admin_setup_fixture::ws_receive() +{ + flat_buffer buffer{}; + network::boost_code ec{}; + BOOST_CHECK(websocket_.has_value()); + + websocket_.value().read(buffer, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + + const auto data = pointer_cast(buffer.data().data()); + return { data, std::next(data, buffer.data().size()) }; +} + +std::string admin_setup_fixture::ws_receive_text() +{ + return to_string(ws_receive()); +} + +boost::json::value admin_setup_fixture::ws_receive_json() +{ + return test::parse_json(ws_receive_text()); +} + +bool admin_setup_fixture::ws_dropped(std::string_view message) +{ + network::boost_code ec{}; + BOOST_CHECK(websocket_.has_value()); + + websocket_.value().write(net::buffer(message), ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + + flat_buffer buffer{}; + websocket_.value().read(buffer, ec); + return ec == boost::asio::error::eof; +} + +std::string admin_setup_fixture::ws_get_text(std::string_view message) +{ + network::boost_code ec{}; + BOOST_CHECK(websocket_.has_value()); + + websocket_.value().write(net::buffer(message), ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + + return to_string(ws_receive()); +} + +boost::json::value admin_setup_fixture::ws_get_json( + std::string_view message) +{ + return test::parse_json(ws_get_text(message)); +} + +data_chunk admin_setup_fixture::ws_get_data(std::string_view message) +{ + network::boost_code ec{}; + BOOST_CHECK(websocket_.has_value()); + + websocket_.value().write(net::buffer(message), ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + + return ws_receive(); +} + +void admin_setup_fixture::write(uint8_t level, const std::string& message) +{ + log_.write(level) << message; +} + +void admin_setup_fixture::fire(uint8_t event_, uint64_t value) +{ + log_.fire(event_, value); +} \ No newline at end of file diff --git a/test/protocols/admin/admin_setup_fixture.hpp b/test/protocols/admin/admin_setup_fixture.hpp new file mode 100644 index 00000000..8d7309eb --- /dev/null +++ b/test/protocols/admin/admin_setup_fixture.hpp @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_SERVER_TEST_PROTOCOLS_ADMIN_ADMIN_SETUP_FIXTURE +#define LIBBITCOIN_SERVER_TEST_PROTOCOLS_ADMIN_ADMIN_SETUP_FIXTURE + +#include "../../test.hpp" +#include "../../mocks/blocks.hpp" + +#define ADMIN_ENDPOINT "127.0.0.1:65001" + +// TODO: admin is http so use boost::beast. + +struct admin_setup_fixture +{ + using status = boost::beast::http::status; + using initializer = std::function; + + DELETE_COPY_MOVE(admin_setup_fixture); + explicit admin_setup_fixture(const initializer& setup); + ~admin_setup_fixture(); + + bool expect_dropped(std::string_view target); + status get_status(std::string_view target); + + std::string get_text(std::string_view target); + system::data_chunk get_data(std::string_view target); + boost::json::value get_json(std::string_view target); + + network::boost_code ws_upgrade(); + system::data_chunk ws_receive(); + std::string ws_receive_text(); + boost::json::value ws_receive_json(); + bool ws_dropped(std::string_view message); + std::string ws_get_text(std::string_view message); + boost::json::value ws_get_json(std::string_view message); + system::data_chunk ws_get_data(std::string_view message); + + // Fire logger message/event (admin stream sources). + void write(uint8_t level, const std::string& message); + void fire(uint8_t event_, uint64_t value=zero); + +protected: + server::configuration config_; + test::store_t store_; + test::query_t query_; + +private: + using string_body = network::http::string_body; + using string_request = boost::beast::http::request; + static string_request create_request(std::string_view target); + + using tcp_stream = boost::beast::tcp_stream; + using websocket_stream = boost::beast::websocket::stream; + + network::logger log_; + server::server_node server_; + boost::asio::io_context io{}; + boost::beast::tcp_stream socket_{ io.get_executor() }; + std::optional websocket_{}; +}; + +struct admin_ten_block_setup_fixture + : admin_setup_fixture +{ + inline admin_ten_block_setup_fixture() + : admin_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }) + { + } +}; + +#endif diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp index 63bf20be..c6b3714b 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp @@ -24,23 +24,6 @@ using namespace boost::beast; -namespace { - -// Internal linkage to avoid colliding with the native fixture's parse_json. -boost::json::value parse_json(std::string_view value) -{ - try - { - return boost::json::parse(value); - } - catch (...) - { - return {}; - } -} - -} // namespace - bitcoind_setup_fixture::bitcoind_setup_fixture(const initializer& setup) : config_ { @@ -141,7 +124,7 @@ boost::json::value bitcoind_setup_fixture::rpc(std::string_view method, http::response response{}; http::read(socket_, buffer, response, ec); BOOST_CHECK_MESSAGE(!ec, ec.message()); - return parse_json(response.body()); + return test::parse_json(response.body()); } bitcoind_setup_fixture::status @@ -167,7 +150,7 @@ boost::json::value bitcoind_setup_fixture::rest_json(std::string_view target) http::read(socket_, buffer, response, ec); BOOST_CHECK_MESSAGE(!ec, ec.message()); BOOST_CHECK_EQUAL(response.result(), http::status::ok); - return parse_json(response.body()); + return test::parse_json(response.body()); } std::string bitcoind_setup_fixture::rest_text(std::string_view target) diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp index 1cf03471..dc9ca1b7 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp @@ -16,13 +16,13 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -#ifndef LIBBITCOIN_SERVER_TEST_PROTOCOLS_BITCOIND_BITCOIND -#define LIBBITCOIN_SERVER_TEST_PROTOCOLS_BITCOIND_BITCOIND +#ifndef LIBBITCOIN_SERVER_TEST_PROTOCOLS_BITCOIND_BITCOIND_SETUP_FIXTURE +#define LIBBITCOIN_SERVER_TEST_PROTOCOLS_BITCOIND_BITCOIND_SETUP_FIXTURE #include "../../test.hpp" #include "../../mocks/blocks.hpp" -#define BITCOIND_ENDPOINT "127.0.0.1:65000" +#define BITCOIND_ENDPOINT "127.0.0.1:65003" struct bitcoind_setup_fixture { diff --git a/test/protocols/electrum/electrum_setup_fixture.hpp b/test/protocols/electrum/electrum_setup_fixture.hpp index 2db26ef1..81f97625 100644 --- a/test/protocols/electrum/electrum_setup_fixture.hpp +++ b/test/protocols/electrum/electrum_setup_fixture.hpp @@ -16,13 +16,13 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -#ifndef LIBBITCOIN_SERVER_TEST_PROTOCOLS_ELECTRUM_ELECTRUM -#define LIBBITCOIN_SERVER_TEST_PROTOCOLS_ELECTRUM_ELECTRUM +#ifndef LIBBITCOIN_SERVER_TEST_PROTOCOLS_ELECTRUM_ELECTRUM_SETUP_FIXTURE +#define LIBBITCOIN_SERVER_TEST_PROTOCOLS_ELECTRUM_ELECTRUM_SETUP_FIXTURE #include "../../test.hpp" #include "../../mocks/blocks.hpp" -#define ELECTRUM_ENDPOINT "127.0.0.1:65000" +#define ELECTRUM_ENDPOINT "127.0.0.1:65002" struct electrum_setup_fixture { diff --git a/test/protocols/native/native_block.cpp b/test/protocols/native/native_block.cpp index 81f42ee8..fba81750 100644 --- a/test/protocols/native/native_block.cpp +++ b/test/protocols/native/native_block.cpp @@ -46,15 +46,15 @@ BOOST_AUTO_TEST_CASE(native__top__data__expected) BOOST_REQUIRE_EQUAL(body, base16_chunk("09")); } -BOOST_AUTO_TEST_CASE(native__top__xml__not_acceptable) +BOOST_AUTO_TEST_CASE(native__top__xml__bad_request) { const auto status = get_status("/v1/top?format=xml"); - BOOST_REQUIRE_EQUAL(status, http::status::not_acceptable); + BOOST_REQUIRE_EQUAL(status, http::status::bad_request); } BOOST_AUTO_TEST_CASE(native__top__default__not_acceptable) { - const auto status = get_status("/v1/top?format=xml"); + const auto status = get_status("/v1/top"); BOOST_REQUIRE_EQUAL(status, http::status::not_acceptable); } diff --git a/test/protocols/native/native_setup_fixture.cpp b/test/protocols/native/native_setup_fixture.cpp index 35b10fce..4c4fcfcc 100644 --- a/test/protocols/native/native_setup_fixture.cpp +++ b/test/protocols/native/native_setup_fixture.cpp @@ -109,19 +109,6 @@ native_setup_fixture::~native_setup_fixture() BC_POP_WARNING() -// utility -boost::json::value parse_json(std::string_view value) -{ - try - { - return boost::json::parse(value); - } - catch (...) - { - return {}; - } -} - native_setup_fixture::string_request native_setup_fixture::create_request(std::string_view target) { @@ -199,7 +186,7 @@ boost::json::value native_setup_fixture::get_json(std::string_view target) BOOST_CHECK_MESSAGE(!ec, ec.message()); BOOST_CHECK_EQUAL(response.result(), http::status::ok); - return parse_json(response.body()); + return test::parse_json(response.body()); } network::boost_code native_setup_fixture::ws_upgrade() @@ -253,7 +240,7 @@ std::string native_setup_fixture::ws_get_text(std::string_view message) boost::json::value native_setup_fixture::ws_get_json( std::string_view message) { - return parse_json(ws_get_text(message)); + return test::parse_json(ws_get_text(message)); } data_chunk native_setup_fixture::ws_get_data(std::string_view message) diff --git a/test/protocols/native/native_setup_fixture.hpp b/test/protocols/native/native_setup_fixture.hpp index 1ad234bb..66a85747 100644 --- a/test/protocols/native/native_setup_fixture.hpp +++ b/test/protocols/native/native_setup_fixture.hpp @@ -16,13 +16,13 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -#ifndef LIBBITCOIN_SERVER_TEST_PROTOCOLS_NATIVE_NATIVE -#define LIBBITCOIN_SERVER_TEST_PROTOCOLS_NATIVE_NATIVE +#ifndef LIBBITCOIN_SERVER_TEST_PROTOCOLS_NATIVE_NATIVE_SETUP_FIXTURE +#define LIBBITCOIN_SERVER_TEST_PROTOCOLS_NATIVE_NATIVE_SETUP_FIXTURE #include "../../test.hpp" #include "../../mocks/blocks.hpp" -#define NATIVE_ENDPOINT "127.0.0.1:65001" +#define NATIVE_ENDPOINT "127.0.0.1:65000" // TODO: native is http so use boost::beast.