aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/build.yml2
-rw-r--r--README.md2
-rw-r--r--contrib/epee/include/net/abstract_tcp_server2.inl1
-rw-r--r--contrib/epee/include/net/levin_protocol_handler_async.h16
-rw-r--r--contrib/epee/src/http_auth.cpp13
-rw-r--r--src/crypto/hash.h3
-rw-r--r--src/cryptonote_core/cryptonote_core.cpp10
-rw-r--r--src/cryptonote_core/cryptonote_core.h2
-rw-r--r--src/cryptonote_core/tx_pool.cpp13
-rw-r--r--src/cryptonote_core/tx_pool.h2
-rw-r--r--src/cryptonote_protocol/cryptonote_protocol_handler.inl2
-rw-r--r--src/daemon/command_line_args.h12
-rw-r--r--src/daemon/daemon.cpp66
-rw-r--r--src/daemon/main.cpp2
-rw-r--r--src/rpc/CMakeLists.txt2
-rw-r--r--src/rpc/daemon_handler.cpp20
-rw-r--r--src/rpc/daemon_handler.h3
-rw-r--r--src/rpc/zmq_restricted_methods.cpp73
-rw-r--r--src/rpc/zmq_restricted_methods.h45
-rw-r--r--src/wallet/wallet_rpc_server.cpp13
-rw-r--r--src/wallet/wallet_rpc_server_commands_defs.h19
-rw-r--r--tests/unit_tests/epee_levin_protocol_handler_async.cpp22
-rw-r--r--tests/unit_tests/http.cpp37
-rw-r--r--tests/unit_tests/zmq_rpc.cpp18
24 files changed, 334 insertions, 64 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d48cbc5f5..489d0ff78 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -86,7 +86,7 @@ jobs:
image: archlinux:latest
steps:
- name: install dependencies
- run: pacman -Syyu --noconfirm base-devel git cmake boost openssl zeromq unbound libsodium readline expat gtest python3 doxygen graphviz hidapi libusb protobuf
+ run: pacman -Syyu --noconfirm base-devel git cmake boost boost-libs openssl zeromq unbound libsodium readline expat gtest python3 doxygen graphviz hidapi libusb protobuf
- name: configure git
run: git config --global --add safe.directory '*'
- uses: actions/checkout@v4
diff --git a/README.md b/README.md
index 6f2562ce4..cc986a71e 100644
--- a/README.md
+++ b/README.md
@@ -209,7 +209,7 @@ sudo apt update && sudo apt install build-essential cmake pkg-config libssl-dev
Install all dependencies at once on Arch:
```
-sudo pacman -Syu --needed base-devel cmake boost openssl zeromq libpgm unbound libsodium libunwind xz readline expat gtest python3 ccache doxygen graphviz qt5-tools hidapi libusb protobuf systemd
+sudo pacman -Syu --needed base-devel cmake boost boost-libs openssl zeromq libpgm unbound libsodium libunwind xz readline expat gtest python3 ccache doxygen graphviz qt5-tools hidapi libusb protobuf systemd
```
Install all dependencies at once on Fedora:
diff --git a/contrib/epee/include/net/abstract_tcp_server2.inl b/contrib/epee/include/net/abstract_tcp_server2.inl
index f65435e72..814ad91b1 100644
--- a/contrib/epee/include/net/abstract_tcp_server2.inl
+++ b/contrib/epee/include/net/abstract_tcp_server2.inl
@@ -516,6 +516,7 @@ namespace net_utils
start_write();
}
});
+ return;
}
}
diff --git a/contrib/epee/include/net/levin_protocol_handler_async.h b/contrib/epee/include/net/levin_protocol_handler_async.h
index 86cee9a11..756560c2a 100644
--- a/contrib/epee/include/net/levin_protocol_handler_async.h
+++ b/contrib/epee/include/net/levin_protocol_handler_async.h
@@ -481,18 +481,26 @@ public:
return false;
}
- temp = std::move(m_fragment_buffer);
- m_fragment_buffer.clear();
+ temp.swap(m_fragment_buffer);
std::memcpy(std::addressof(m_current_head), std::addressof(temp[0]), sizeof(bucket_head2));
+ const std::uint64_t inner_size = SWAP64LE(m_current_head.m_cb);
+ buff_to_invoke = {reinterpret_cast<const uint8_t*>(temp.data()) + sizeof(bucket_head2), temp.size() - sizeof(bucket_head2)};
+ if (buff_to_invoke.size() < inner_size)
+ {
+ MERROR(m_connection_context << "Invalid fragmented buffer size: " << buff_to_invoke.size() << " vs " << inner_size);
+ return false;
+ }
+
+ buff_to_invoke = {buff_to_invoke.data(), std::size_t(inner_size)};
+
const size_t max_bytes = m_connection_context.get_max_bytes(m_current_head.m_command);
- if(m_current_head.m_cb > std::min<size_t>(max_packet_size, max_bytes))
+ if(buff_to_invoke.size() > std::min<size_t>(max_packet_size, max_bytes))
{
MERROR(m_connection_context << "Maximum packet size exceed!, m_max_packet_size = " << std::min<size_t>(max_packet_size, max_bytes)
<< ", packet header received " << m_current_head.m_cb << ", command " << m_current_head.m_command
<< ", connection will be closed.");
return false;
}
- buff_to_invoke = {reinterpret_cast<const uint8_t*>(temp.data()) + sizeof(bucket_head2), temp.size() - sizeof(bucket_head2)};
}
bool is_response = (m_oponent_protocol_ver == LEVIN_PROTOCOL_VER_1 && m_current_head.m_flags&LEVIN_PACKET_RESPONSE);
diff --git a/contrib/epee/src/http_auth.cpp b/contrib/epee/src/http_auth.cpp
index 614004ffa..dfde408cb 100644
--- a/contrib/epee/src/http_auth.cpp
+++ b/contrib/epee/src/http_auth.cpp
@@ -64,6 +64,7 @@
#include <iterator>
#include <limits>
#include <openssl/evp.h>
+#include <openssl/rand.h>
#include <tuple>
#include <type_traits>
@@ -296,13 +297,21 @@ namespace
std::array<char, 8> nc{{}};
boost::copy(out, nc.data());
+
+ std::array<uint8_t, 16> rbuf{{}};
+ if (RAND_bytes(rbuf.data(), rbuf.size()) != 1)
+ return {};
+
+ const std::string cnonce = epee::string_encoding::base64_encode(rbuf.data(), rbuf.size());
const auto response = digest(
- generate_a1(digest, user), u8":", user.server.nonce, u8":", nc, u8"::auth:", digest(method, u8":", uri)
+ generate_a1(digest, user), u8":", user.server.nonce, u8":", nc, u8":", cnonce, u8":auth:", digest(method, u8":", uri)
);
+
out.clear();
init_client_value(out, Digest::name, user, uri, response);
add_field(out, u8"qop", ceref(u8"auth"));
add_field(out, u8"nc", nc);
+ add_field(out, u8"cnonce", quoted_(cnonce));
return out;
}
@@ -592,7 +601,7 @@ namespace
boost::equals((*digest).name, request.algorithm, ascii_iequal)
);
if (request.qop.empty())
- value_generator = old_algorithm<digest_type>{*digest};
+ index = boost::fusion::size(digest_algorithms);
else
{
for (auto elem = boost::make_split_iterator(request.qop, boost::token_finder(http_list_separator));
diff --git a/src/crypto/hash.h b/src/crypto/hash.h
index 8ea626314..274907076 100644
--- a/src/crypto/hash.h
+++ b/src/crypto/hash.h
@@ -101,6 +101,9 @@ namespace crypto {
constexpr static crypto::hash null_hash = {};
constexpr static crypto::hash8 null_hash8 = {};
+
+ inline bool operator<(const hash &lhs, const hash &rhs) noexcept { return memcmp(&lhs, &rhs, sizeof(hash)) < 0; }
+ inline bool operator>(const hash &lhs, const hash &rhs) noexcept { return rhs < lhs; }
}
CRYPTO_MAKE_HASHABLE(hash)
diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp
index d30bc72a7..627f11450 100644
--- a/src/cryptonote_core/cryptonote_core.cpp
+++ b/src/cryptonote_core/cryptonote_core.cpp
@@ -1349,7 +1349,11 @@ namespace cryptonote
bool core::prepare_handle_incoming_blocks(const std::vector<block_complete_entry> &blocks_entry, std::vector<block> &blocks)
{
m_incoming_tx_lock.lock();
- if (!m_blockchain_storage.prepare_handle_incoming_blocks(blocks_entry, blocks))
+ bool success = false;
+ try { success = m_blockchain_storage.prepare_handle_incoming_blocks(blocks_entry, blocks); }
+ catch (const std::exception &e) { MERROR("Failed prepare handle incoming blocks: " << e.what()); }
+ catch (...) { MERROR("Failed prepare handling incoming blocks"); }
+ if (!success)
{
cleanup_handle_incoming_blocks(false);
return false;
@@ -1843,9 +1847,9 @@ namespace cryptonote
m_blockchain_storage.flush_invalid_blocks();
}
//-----------------------------------------------------------------------------------------------
- bool core::get_txpool_complement(const std::vector<crypto::hash> &hashes, std::vector<cryptonote::blobdata> &txes)
+ bool core::get_txpool_complement(std::vector<crypto::hash> hashes, std::vector<cryptonote::blobdata> &txes)
{
- return m_mempool.get_complement(hashes, txes);
+ return m_mempool.get_complement(std::move(hashes), txes);
}
//-----------------------------------------------------------------------------------------------
bool core::update_blockchain_pruning()
diff --git a/src/cryptonote_core/cryptonote_core.h b/src/cryptonote_core/cryptonote_core.h
index ab9a0ee12..f1339dbb9 100644
--- a/src/cryptonote_core/cryptonote_core.h
+++ b/src/cryptonote_core/cryptonote_core.h
@@ -898,7 +898,7 @@ namespace cryptonote
*
* @return true iff success, false otherwise
*/
- bool get_txpool_complement(const std::vector<crypto::hash> &hashes, std::vector<cryptonote::blobdata> &txes);
+ bool get_txpool_complement(std::vector<crypto::hash> hashes, std::vector<cryptonote::blobdata> &txes);
/**
* @brief validates some simple properties of a transaction
diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp
index 40c004da4..3bc508037 100644
--- a/src/cryptonote_core/tx_pool.cpp
+++ b/src/cryptonote_core/tx_pool.cpp
@@ -668,17 +668,24 @@ namespace cryptonote
return true;
}
//---------------------------------------------------------------------------------
- bool tx_memory_pool::get_complement(const std::vector<crypto::hash> &hashes, std::vector<cryptonote::blobdata> &txes) const
+ bool tx_memory_pool::get_complement(std::vector<crypto::hash> hashes, std::vector<cryptonote::blobdata> &txes) const
{
CRITICAL_REGION_LOCAL(m_transactions_lock);
CRITICAL_REGION_LOCAL1(m_blockchain);
+ // Sort so we can do binary search later
+ std::sort(hashes.begin(), hashes.end());
+
m_blockchain.for_all_txpool_txes([this, &hashes, &txes](const crypto::hash &txid, const txpool_tx_meta_t &meta, const cryptonote::blobdata_ref*) {
const auto tx_relay_method = meta.get_relay_method();
if (tx_relay_method != relay_method::block && tx_relay_method != relay_method::fluff)
return true;
- const auto i = std::find(hashes.begin(), hashes.end(), txid);
- if (i == hashes.end())
+
+ // Do binary search for our pool TXID in given list, skip to next if already present
+ const auto hash_it = std::lower_bound(hashes.cbegin(), hashes.cend(), txid);
+ if (hash_it != hashes.cend() && *hash_it == txid)
+ return true;
+
{
cryptonote::blobdata bd;
try
diff --git a/src/cryptonote_core/tx_pool.h b/src/cryptonote_core/tx_pool.h
index df79aea1d..ed75a3dfc 100644
--- a/src/cryptonote_core/tx_pool.h
+++ b/src/cryptonote_core/tx_pool.h
@@ -489,7 +489,7 @@ namespace cryptonote
/**
* @brief get transactions not in the passed set
*/
- bool get_complement(const std::vector<crypto::hash> &hashes, std::vector<cryptonote::blobdata> &txes) const;
+ bool get_complement(std::vector<crypto::hash> hashes, std::vector<cryptonote::blobdata> &txes) const;
/**
* @brief get info necessary for update of pool-related info in a wallet, preferably incremental
diff --git a/src/cryptonote_protocol/cryptonote_protocol_handler.inl b/src/cryptonote_protocol/cryptonote_protocol_handler.inl
index fb195b390..6a3fd2532 100644
--- a/src/cryptonote_protocol/cryptonote_protocol_handler.inl
+++ b/src/cryptonote_protocol/cryptonote_protocol_handler.inl
@@ -855,7 +855,7 @@ namespace cryptonote
std::vector<cryptonote::blobdata> local_txs;
std::vector<cryptonote::blobdata> txes;
- if (!m_core.get_txpool_complement(arg.hashes, txes))
+ if (!m_core.get_txpool_complement(std::move(arg.hashes), txes))
{
LOG_ERROR_CCONTEXT("failed to get txpool complement");
return 1;
diff --git a/src/daemon/command_line_args.h b/src/daemon/command_line_args.h
index 96fddc02d..a0b15d021 100644
--- a/src/daemon/command_line_args.h
+++ b/src/daemon/command_line_args.h
@@ -136,6 +136,18 @@ namespace daemon_args
, "Address for ZMQ pub - tcp://ip:port or ipc://path"
};
+ const command_line::arg_descriptor<bool> arg_restricted_zmq_rpc = {
+ "restricted-zmq-rpc"
+ , "Restrict ZMQ RPC to view-only / non-sensitive methods"
+ , false
+ };
+
+ const command_line::arg_descriptor<bool> arg_confirm_zmq_rpc_external_bind = {
+ "confirm-zmq-rpc-external-bind"
+ , "Confirm zmq-rpc-bind-ip value is NOT a loopback (local) IP"
+ , false
+ };
+
const command_line::arg_descriptor<bool> arg_zmq_rpc_disabled = {
"no-zmq"
, "Disable ZMQ RPC server"
diff --git a/src/daemon/daemon.cpp b/src/daemon/daemon.cpp
index 2512e5430..e069d1b65 100644
--- a/src/daemon/daemon.cpp
+++ b/src/daemon/daemon.cpp
@@ -31,6 +31,7 @@
#include <memory>
#include <stdexcept>
#include <boost/algorithm/string/split.hpp>
+#include <boost/asio/ip/address.hpp>
#include "misc_log_ex.h"
#include "daemon/daemon.h"
#include "rpc/daemon_handler.h"
@@ -58,10 +59,39 @@ using namespace epee;
namespace daemonize {
+namespace
+{
+ void verify_zmq_rpc_bind(const boost::program_options::variables_map& vm)
+ {
+ std::string bind_ip = command_line::get_arg(vm, daemon_args::arg_zmq_rpc_bind_ip);
+ if (bind_ip.empty())
+ return;
+
+ // ZMQ bind input already accepts bracketed IPv6 literals, but
+ // boost::asio::ip::make_address does not.
+ if (bind_ip.size() >= 2 && bind_ip.front() == '[' && bind_ip.back() == ']')
+ bind_ip = bind_ip.substr(1, bind_ip.size() - 2);
+
+ boost::system::error_code ec{};
+ const auto parsed_ip = boost::asio::ip::make_address(bind_ip, ec);
+ if (ec)
+ throw std::runtime_error{"Invalid IP address given for --" + std::string(daemon_args::arg_zmq_rpc_bind_ip.name)};
+
+ if (!parsed_ip.is_loopback() && !command_line::get_arg(vm, daemon_args::arg_confirm_zmq_rpc_external_bind))
+ {
+ throw std::runtime_error{
+ std::string{"--"} + daemon_args::arg_zmq_rpc_bind_ip.name +
+ " permits inbound unencrypted external connections. Consider SSH tunnel or SSL proxy instead. Override with --" +
+ daemon_args::arg_confirm_zmq_rpc_external_bind.name
+ };
+ }
+ }
+}
+
struct zmq_internals
{
- explicit zmq_internals(t_core& core, t_p2p& p2p)
- : rpc_handler{core.get(), p2p.get()}
+ explicit zmq_internals(t_core& core, t_p2p& p2p, const bool restricted)
+ : rpc_handler{core.get(), p2p.get(), restricted}
, server{rpc_handler}
{}
@@ -104,7 +134,10 @@ public:
if (!command_line::get_arg(vm, daemon_args::arg_zmq_rpc_disabled))
{
- zmq.reset(new zmq_internals{core, p2p});
+ verify_zmq_rpc_bind(vm);
+
+ const bool restricted = command_line::get_arg(vm, daemon_args::arg_restricted_zmq_rpc);
+ zmq.reset(new zmq_internals{core, p2p, restricted});
const std::string zmq_port = command_line::get_arg(vm, daemon_args::arg_zmq_rpc_bind_port);
const std::string zmq_address = command_line::get_arg(vm, daemon_args::arg_zmq_rpc_bind_ip);
@@ -124,6 +157,33 @@ public:
core.get().get_blockchain_storage().add_miner_notify(cryptonote::listener::zmq_pub::miner_data{shared});
}
}
+ else // if --no-zmq specified
+ {
+ // Assert that none of --zmq-rpc-bind-port, --zmq-rpc-bind-ip, and --zmq-pub are specified b/c
+ // that does not make semantic sense with --no-zmq.
+ if (command_line::get_arg(vm, daemon_args::arg_zmq_rpc_bind_port) !=
+ daemon_args::arg_zmq_rpc_bind_port.default_value)
+ {
+ MWARNING("WARN: --zmq-rpc-bind-port has no effect because --no-zmq was specified");
+ }
+ if (command_line::get_arg(vm, daemon_args::arg_zmq_rpc_bind_ip) !=
+ daemon_args::arg_zmq_rpc_bind_ip.default_value)
+ {
+ MWARNING("WARN: --zmq-rpc-bind-ip has no effect because --no-zmq was specified");
+ }
+ if (command_line::get_arg(vm, daemon_args::arg_confirm_zmq_rpc_external_bind))
+ {
+ MWARNING("WARN: --confirm-zmq-rpc-external-bind has no effect because --no-zmq was specified");
+ }
+ if (command_line::get_arg(vm, daemon_args::arg_restricted_zmq_rpc))
+ {
+ MWARNING("WARN: --restricted-zmq-rpc has no effect because --no-zmq was specified");
+ }
+ if (!command_line::get_arg(vm, daemon_args::arg_zmq_pub).empty())
+ {
+ MWARNING("WARN: --zmq-pub has no effect because --no-zmq was specified");
+ }
+ }
}
};
diff --git a/src/daemon/main.cpp b/src/daemon/main.cpp
index 38b99ff49..39ef04772 100644
--- a/src/daemon/main.cpp
+++ b/src/daemon/main.cpp
@@ -158,6 +158,8 @@ int main(int argc, char const * argv[])
command_line::add_arg(core_settings, daemon_args::arg_zmq_rpc_bind_ip);
command_line::add_arg(core_settings, daemon_args::arg_zmq_rpc_bind_port);
command_line::add_arg(core_settings, daemon_args::arg_zmq_pub);
+ command_line::add_arg(core_settings, daemon_args::arg_confirm_zmq_rpc_external_bind);
+ command_line::add_arg(core_settings, daemon_args::arg_restricted_zmq_rpc);
command_line::add_arg(core_settings, daemon_args::arg_zmq_rpc_disabled);
daemonizer::init_options(hidden_options, visible_options);
diff --git a/src/rpc/CMakeLists.txt b/src/rpc/CMakeLists.txt
index edfc70067..f12da48d3 100644
--- a/src/rpc/CMakeLists.txt
+++ b/src/rpc/CMakeLists.txt
@@ -49,6 +49,7 @@ set(rpc_pub_sources zmq_pub.cpp)
set(daemon_rpc_server_sources
daemon_handler.cpp
+ zmq_restricted_methods.cpp
zmq_pub.cpp
zmq_server.cpp)
@@ -81,6 +82,7 @@ set(daemon_rpc_server_private_headers
message.h
daemon_messages.h
daemon_handler.h
+ zmq_restricted_methods.h
zmq_server.h)
diff --git a/src/rpc/daemon_handler.cpp b/src/rpc/daemon_handler.cpp
index c1e4c10bf..671889ddd 100644
--- a/src/rpc/daemon_handler.cpp
+++ b/src/rpc/daemon_handler.cpp
@@ -27,6 +27,7 @@
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "daemon_handler.h"
+#include "rpc/zmq_restricted_methods.h"
#include <algorithm>
#include <cstring>
@@ -109,12 +110,14 @@ namespace rpc
};
} // anonymous
- DaemonHandler::DaemonHandler(cryptonote::core& c, t_p2p& p2p)
- : m_core(c), m_p2p(p2p)
+ DaemonHandler::DaemonHandler(cryptonote::core& c, t_p2p& p2p, bool restricted)
+ : m_core(c), m_p2p(p2p), m_restricted(restricted)
{
const auto last_sorted = std::is_sorted_until(std::begin(handlers), std::end(handlers));
if (last_sorted != std::end(handlers))
throw std::logic_error{std::string{"ZMQ JSON-RPC handlers map is not properly sorted, see "} + last_sorted->method_name};
+
+ check_blocked_methods_sorted();
}
void DaemonHandler::handle(const GetHeight::Request& req, GetHeight::Response& res)
@@ -930,13 +933,24 @@ namespace rpc
epee::byte_slice DaemonHandler::handle(std::string&& request)
{
- MDEBUG("Handling RPC request: " << request);
+ if (m_restricted)
+ MDEBUG("Handling RPC request");
+ else
+ MDEBUG("Handling RPC request: " << request);
try
{
FullMessage req_full(std::move(request), true);
const std::string request_type = req_full.getRequestType();
+ if (m_restricted && is_blocked_in_restricted_mode(request_type))
+ {
+ Message fail;
+ fail.status = Message::STATUS_FAILED;
+ fail.error_details = "\"" + request_type + "\" is not available in restricted mode.";
+ return FullMessage::getResponse(fail, req_full.getID());
+ }
+
const auto matched_handler = std::lower_bound(std::begin(handlers), std::end(handlers), request_type);
if (matched_handler == std::end(handlers) || matched_handler->method_name != request_type)
return BAD_REQUEST(request_type, req_full.getID());
diff --git a/src/rpc/daemon_handler.h b/src/rpc/daemon_handler.h
index 74885cf30..f0ddb2967 100644
--- a/src/rpc/daemon_handler.h
+++ b/src/rpc/daemon_handler.h
@@ -51,7 +51,7 @@ class DaemonHandler : public RpcHandler
{
public:
- DaemonHandler(cryptonote::core& c, t_p2p& p2p);
+ DaemonHandler(cryptonote::core& c, t_p2p& p2p, bool restricted = false);
~DaemonHandler() { }
@@ -143,6 +143,7 @@ class DaemonHandler : public RpcHandler
cryptonote::core& m_core;
t_p2p& m_p2p;
+ bool m_restricted;
};
} // namespace rpc
diff --git a/src/rpc/zmq_restricted_methods.cpp b/src/rpc/zmq_restricted_methods.cpp
new file mode 100644
index 000000000..eb58fd3d2
--- /dev/null
+++ b/src/rpc/zmq_restricted_methods.cpp
@@ -0,0 +1,73 @@
+// Copyright (c) 2016-2026, The Monero Project
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+// of conditions and the following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "rpc/zmq_restricted_methods.h"
+
+#include <algorithm>
+#include <array>
+
+namespace cryptonote
+{
+namespace rpc
+{
+ namespace
+ {
+ const std::array<boost::string_ref, 9> blocked_in_restricted_mode{{
+ "flush_txpool",
+ "get_peer_list",
+ "mining_status",
+ "relay_tx",
+ "save_bc",
+ "set_log_categories",
+ "set_log_level",
+ "start_mining",
+ "stop_mining"
+ }};
+ }
+
+ bool is_blocked_in_restricted_mode(const boost::string_ref method) noexcept
+ {
+ return std::binary_search(
+ blocked_in_restricted_mode.begin(),
+ blocked_in_restricted_mode.end(),
+ method
+ );
+ }
+
+ void check_blocked_methods_sorted()
+ {
+ const auto last =
+ std::is_sorted_until(blocked_in_restricted_mode.begin(), blocked_in_restricted_mode.end());
+
+ if (last != blocked_in_restricted_mode.end())
+ throw std::logic_error{
+ std::string{"ZMQ restricted-method map is not properly sorted, see "} + last->to_string()
+ };
+ }
+} // rpc
+} // cryptonote
diff --git a/src/rpc/zmq_restricted_methods.h b/src/rpc/zmq_restricted_methods.h
new file mode 100644
index 000000000..28f2dd0a1
--- /dev/null
+++ b/src/rpc/zmq_restricted_methods.h
@@ -0,0 +1,45 @@
+// Copyright (c) 2016-2026, The Monero Project
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+// of conditions and the following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+#include <stdexcept>
+#include <boost/utility/string_ref.hpp>
+
+namespace cryptonote
+{
+namespace rpc
+{
+ //! Returns true when `method` must be rejected while ZMQ RPC runs in
+ //! restricted mode. Keep this list in sync with daemon RPC method
+ bool is_blocked_in_restricted_mode(boost::string_ref method) noexcept;
+
+ //! Throws std::logic_error if the internal method table is not sorted.
+ void check_blocked_methods_sorted();
+} // rpc
+} // cryptonote
diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp
index 0cf75a1c4..531dc3197 100644
--- a/src/wallet/wallet_rpc_server.cpp
+++ b/src/wallet/wallet_rpc_server.cpp
@@ -1517,7 +1517,7 @@ namespace tools
for (size_t n = 0; n < tx_constructions.size(); ++n)
{
const tools::wallet2::tx_construction_data &cd = tx_constructions[n];
- res.desc.push_back({0, 0, std::numeric_limits<uint32_t>::max(), 0, {}, "", 0, "", 0, 0, ""});
+ res.desc.push_back({0, 0, std::numeric_limits<uint32_t>::max(), 0, {}, {}, "", 0, "", 0, 0, ""});
wallet_rpc::COMMAND_RPC_DESCRIBE_TRANSFER::transfer_description &desc = res.desc.back();
// Clear the recipients collection ready for this loop iteration
tx_dests.clear();
@@ -1548,8 +1548,15 @@ namespace tools
for (size_t s = 0; s < cd.sources.size(); ++s)
{
- desc.amount_in += cd.sources[s].amount;
- size_t ring_size = cd.sources[s].outputs.size();
+ const cryptonote::tx_source_entry &src_in = cd.sources[s];
+ desc.sources.emplace_back();
+ wallet_rpc::COMMAND_RPC_DESCRIBE_TRANSFER::source &src_out = desc.sources.back();
+ src_out.amount = src_in.amount;
+ src_out.global_index = src_in.outputs.at(src_in.real_output_in_tx_index).first;
+ src_out.rct = src_in.rct;
+ src_out.pubkey = epee::string_tools::pod_to_hex(src_in.outputs.at(src_in.real_output_in_tx_index).second);
+ desc.amount_in += src_in.amount;
+ size_t ring_size = src_in.outputs.size();
if (ring_size < desc.ring_size)
desc.ring_size = ring_size;
}
diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h
index ebcd1135c..bd3321ec0 100644
--- a/src/wallet/wallet_rpc_server_commands_defs.h
+++ b/src/wallet/wallet_rpc_server_commands_defs.h
@@ -47,7 +47,7 @@
// advance which version they will stop working with
// Don't go over 32767 for any of these
#define WALLET_RPC_VERSION_MAJOR 1
-#define WALLET_RPC_VERSION_MINOR 29
+#define WALLET_RPC_VERSION_MINOR 30
#define MAKE_WALLET_RPC_VERSION(major,minor) (((major)<<16)|(minor))
#define WALLET_RPC_VERSION MAKE_WALLET_RPC_VERSION(WALLET_RPC_VERSION_MAJOR, WALLET_RPC_VERSION_MINOR)
namespace tools
@@ -698,6 +698,21 @@ namespace wallet_rpc
struct COMMAND_RPC_DESCRIBE_TRANSFER
{
+ struct source
+ {
+ uint64_t amount;
+ uint64_t global_index;
+ bool rct;
+ std::string pubkey;
+
+ BEGIN_KV_SERIALIZE_MAP()
+ KV_SERIALIZE(amount)
+ KV_SERIALIZE(global_index)
+ KV_SERIALIZE(rct)
+ KV_SERIALIZE(pubkey)
+ END_KV_SERIALIZE_MAP()
+ };
+
struct recipient
{
std::string address;
@@ -715,6 +730,7 @@ namespace wallet_rpc
uint64_t amount_out;
uint32_t ring_size;
uint64_t unlock_time;
+ std::list<source> sources;
std::list<recipient> recipients;
std::string payment_id;
uint64_t change_amount;
@@ -728,6 +744,7 @@ namespace wallet_rpc
KV_SERIALIZE(amount_out)
KV_SERIALIZE(ring_size)
KV_SERIALIZE(unlock_time)
+ KV_SERIALIZE(sources)
KV_SERIALIZE(recipients)
KV_SERIALIZE(payment_id)
KV_SERIALIZE(change_amount)
diff --git a/tests/unit_tests/epee_levin_protocol_handler_async.cpp b/tests/unit_tests/epee_levin_protocol_handler_async.cpp
index 9dbb2c19b..4ea6fde90 100644
--- a/tests/unit_tests/epee_levin_protocol_handler_async.cpp
+++ b/tests/unit_tests/epee_levin_protocol_handler_async.cpp
@@ -506,7 +506,6 @@ TEST_F(positive_test_connection_to_levin_protocol_handler_calls, handler_process
}
std::string compare_buffer(1024 * 4, 'c');
- compare_buffer.resize(((1024 - sizeof(epee::levin::bucket_head2)) * 5) - sizeof(epee::levin::bucket_head2)); // add padding zeroes
ASSERT_EQ(4u, m_commands_handler.notify_counter());
ASSERT_EQ(0u, m_commands_handler.invoke_counter());
@@ -652,3 +651,24 @@ TEST_F(test_levin_protocol_handler__hanle_recv_with_invalid_data, handles_short_
ASSERT_FALSE(m_conn->m_protocol_handler.handle_recv(m_buf.data(), m_buf.size()));
}
+
+TEST_F(test_levin_protocol_handler__hanle_recv_with_invalid_data, handles_bad_cb)
+{
+ m_req_head.m_cb = sizeof(epee::levin::bucket_head2);
+ m_req_head.m_flags = LEVIN_PACKET_BEGIN;
+ m_req_head.m_command = 0;
+
+ epee::levin::bucket_head2 inner{};
+ inner.m_cb = 2;
+ m_in_data.resize(sizeof(epee::levin::bucket_head2));
+ prepare_buf();
+
+ ASSERT_TRUE(m_conn->m_protocol_handler.handle_recv(m_buf.data(), m_buf.size()));
+
+ m_req_head.m_cb = 1;
+ m_req_head.m_flags = LEVIN_PACKET_END;
+ m_in_data.resize(1);
+ prepare_buf();
+
+ ASSERT_FALSE(m_conn->m_protocol_handler.handle_recv(m_buf.data(), m_buf.size()));
+}
diff --git a/tests/unit_tests/http.cpp b/tests/unit_tests/http.cpp
index f8eb0127b..1746ee190 100644
--- a/tests/unit_tests/http.cpp
+++ b/tests/unit_tests/http.cpp
@@ -614,10 +614,8 @@ TEST(HTTP_Client_Auth, BadSyntax)
TEST(HTTP_Client_Auth, MD5)
{
- constexpr char method[] = "NOP";
constexpr char nonce[] = "some crazy nonce";
constexpr char realm[] = "the only realm";
- constexpr char uri[] = "/some_file";
const http::login user{"foo", "bar"};
http::http_client_auth auth{user};
@@ -636,42 +634,11 @@ TEST(HTTP_Client_Auth, MD5)
},
});
- EXPECT_EQ(http::http_client_auth::kSuccess, auth.handle_401(response));
- const auto auth_field = auth.get_auth_field(method, uri);
- ASSERT_TRUE(bool(auth_field));
-
- const auto parsed = parse_fields(auth_field->second);
- EXPECT_STREQ(u8"Authorization", auth_field->first.c_str());
- EXPECT_EQ(parsed.end(), parsed.find(u8"opaque"));
- EXPECT_EQ(parsed.end(), parsed.find(u8"qop"));
- EXPECT_EQ(parsed.end(), parsed.find(u8"nc"));
- EXPECT_STREQ(u8"MD5", parsed.at(u8"algorithm").c_str());
- EXPECT_STREQ(nonce, parsed.at(u8"nonce").c_str());
- EXPECT_STREQ(uri, parsed.at(u8"uri").c_str());
- EXPECT_EQ(user.username, parsed.at(u8"username"));
- EXPECT_STREQ(realm, parsed.at(u8"realm").c_str());
-
- const std::string a1 = get_a1(user, parsed);
- const std::string a2 = get_a2(uri);
- const std::string auth_code = md5_hex(
- boost::join(std::vector<std::string>{md5_hex(a1), nonce, md5_hex(a2)}, u8":")
- );
- EXPECT_TRUE(boost::iequals(auth_code, parsed.at(u8"response")));
- {
- const auto auth_field_dup = auth.get_auth_field(method, uri);
- ASSERT_TRUE(bool(auth_field_dup));
- EXPECT_EQ(*auth_field, *auth_field_dup);
- }
-
-
- EXPECT_EQ(http::http_client_auth::kBadPassword, auth.handle_401(response));
- response.m_header_info.m_etc_fields.front().second.append(u8"," + write_fields({{u8"stale", u8"TRUE"}}));
- EXPECT_EQ(http::http_client_auth::kSuccess, auth.handle_401(response));
+ EXPECT_EQ(http::http_client_auth::kParseFailure, auth.handle_401(response));
}
TEST(HTTP_Client_Auth, MD5_auth)
{
- constexpr char cnonce[] = "";
constexpr char method[] = "NOP";
constexpr char nonce[] = "some crazy nonce";
constexpr char opaque[] = "this is the opaque";
@@ -723,7 +690,7 @@ TEST(HTTP_Client_Auth, MD5_auth)
const std::string a1 = get_a1(user, parsed);
const std::string a2 = get_a2(uri);
const std::string auth_code = md5_hex(
- boost::join(std::vector<std::string>{md5_hex(a1), nonce, nc, cnonce, u8"auth", md5_hex(a2)}, u8":")
+ boost::join(std::vector<std::string>{md5_hex(a1), nonce, nc, parsed.at(u8"cnonce"), u8"auth", md5_hex(a2)}, u8":")
);
EXPECT_TRUE(boost::iequals(auth_code, parsed.at(u8"response")));
}
diff --git a/tests/unit_tests/zmq_rpc.cpp b/tests/unit_tests/zmq_rpc.cpp
index c98f0011b..38b5f083f 100644
--- a/tests/unit_tests/zmq_rpc.cpp
+++ b/tests/unit_tests/zmq_rpc.cpp
@@ -39,6 +39,7 @@
#include "net/zmq.h"
#include "rpc/message.h"
#include "rpc/zmq_pub.h"
+#include "rpc/zmq_restricted_methods.h"
#include "rpc/zmq_server.h"
#include "serialization/json_object.h"
@@ -69,6 +70,23 @@ TEST(ZmqFullMessage, Request)
EXPECT_STREQ("foo", parsed.getRequestType().c_str());
}
+TEST(ZmqRestrictedMethods, BasicCoverage)
+{
+ EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("flush_txpool"));
+ EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("get_peer_list"));
+ EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("mining_status"));
+ EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("relay_tx"));
+ EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("save_bc"));
+ EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("set_log_categories"));
+ EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("set_log_level"));
+ EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("start_mining"));
+ EXPECT_TRUE(cryptonote::rpc::is_blocked_in_restricted_mode("stop_mining"));
+
+ EXPECT_FALSE(cryptonote::rpc::is_blocked_in_restricted_mode("get_height"));
+ EXPECT_FALSE(cryptonote::rpc::is_blocked_in_restricted_mode("get_info"));
+ EXPECT_FALSE(cryptonote::rpc::is_blocked_in_restricted_mode("send_raw_tx"));
+}
+
namespace
{
using published_json = std::pair<std::string, rapidjson::Document>;