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.