mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-29 14:59:39 -04:00
Merge 9ff5bc7565
into c5e44a0435
This commit is contained in:
commit
bf51eef4ae
24 changed files with 407 additions and 86 deletions
|
@ -391,6 +391,12 @@ target_link_libraries(bitcoin_cli
|
|||
# Bitcoin Core RPC client
|
||||
if(BUILD_CLI)
|
||||
add_executable(bitcoin-cli bitcoin-cli.cpp)
|
||||
if(ENABLE_IPC)
|
||||
target_sources(bitcoin-cli PRIVATE init/basic-ipc.cpp)
|
||||
target_link_libraries(bitcoin-cli bitcoin_ipc)
|
||||
else()
|
||||
target_sources(bitcoin-cli PRIVATE init/basic.cpp)
|
||||
endif()
|
||||
add_windows_resources(bitcoin-cli bitcoin-cli-res.rc)
|
||||
target_link_libraries(bitcoin-cli
|
||||
core_interface
|
||||
|
|
|
@ -11,6 +11,9 @@
|
|||
#include <common/system.h>
|
||||
#include <compat/compat.h>
|
||||
#include <compat/stdin.h>
|
||||
#include <interfaces/init.h>
|
||||
#include <interfaces/ipc.h>
|
||||
#include <interfaces/rpc.h>
|
||||
#include <policy/feerate.h>
|
||||
#include <rpc/client.h>
|
||||
#include <rpc/mining.h>
|
||||
|
@ -108,6 +111,7 @@ static void SetupCliArgs(ArgsManager& argsman)
|
|||
argsman.AddArg("-stdin", "Read extra arguments from standard input, one per line until EOF/Ctrl-D (recommended for sensitive information such as passphrases). When combined with -stdinrpcpass, the first line from standard input is used for the RPC password.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
|
||||
argsman.AddArg("-stdinrpcpass", "Read RPC password from standard input as a single line. When combined with -stdin, the first line from standard input is used for the RPC password. When combined with -stdinwalletpassphrase, -stdinrpcpass consumes the first line, and -stdinwalletpassphrase consumes the second.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
|
||||
argsman.AddArg("-stdinwalletpassphrase", "Read wallet passphrase from standard input as a single line. When combined with -stdin, the first line from standard input is used for the wallet passphrase.", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
|
||||
argsman.AddArg("-ipcconnect=<address>", "Connect to bitcoin-node through IPC socket instead of TCP socket to execute requests. Valid <address> values are 'auto' to try to connect to default socket path at <datadir>/node.sock' but fall back to TCP if it is not available, 'unix' to connect to the default socket and fail if it isn't available, or 'unix:<socket path>' to connect to a socket at a nonstandard path. -noipcconnect can be specified to avoid attempting to use IPC at all. Default value: auto", ArgsManager::ALLOW_ANY, OptionsCategory::IPC);
|
||||
}
|
||||
|
||||
std::optional<std::string> RpcWalletName(const ArgsManager& args)
|
||||
|
@ -775,7 +779,40 @@ struct DefaultRequestHandler : BaseRequestHandler {
|
|||
}
|
||||
};
|
||||
|
||||
static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector<std::string>& args, const std::optional<std::string>& rpcwallet = {})
|
||||
static std::optional<UniValue> CallIPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector<std::string>& args, const std::string& endpoint, const std::string& username)
|
||||
{
|
||||
auto ipcconnect{gArgs.GetArg("-ipcconnect", "auto")};
|
||||
if (ipcconnect == "0") return {}; // Do not attempt IPC if -ipcconnect is disabled.
|
||||
if (gArgs.IsArgSet("-rpcconnect") && !gArgs.IsArgNegated("-rpcconnect")) {
|
||||
if (ipcconnect == "auto") return {}; // Use HTTP if -ipcconnect=auto is set and -rpcconnect is enabled.
|
||||
throw std::runtime_error("-rpcconnect and -ipcconnect options cannot both be enabled");
|
||||
}
|
||||
|
||||
std::unique_ptr<interfaces::Init> local_init{interfaces::MakeBasicInit("bitcoin-cli")};
|
||||
if (!local_init || !local_init->ipc()) {
|
||||
if (ipcconnect == "auto") return {}; // Use HTTP if -ipcconnect=auto is set and there is no IPC support.
|
||||
throw std::runtime_error("bitcoin-cli was not built with IPC support");
|
||||
}
|
||||
|
||||
std::unique_ptr<interfaces::Init> node_init;
|
||||
try {
|
||||
node_init = local_init->ipc()->connectAddress(ipcconnect);
|
||||
if (!node_init) return {}; // Fall back to HTTP if -ipcconnect=auto connect failed.
|
||||
} catch (const std::exception& e) {
|
||||
// Catch connect error if -ipcconnect=unix was specified
|
||||
throw std::runtime_error{strprintf("%s\n\n"
|
||||
"Probably bitcoin-node is not running or not listening on a unix socket. Can be started with:\n\n"
|
||||
" bitcoin-node -chain=%s -ipcbind=unix", e.what(), gArgs.GetChainTypeString())};
|
||||
}
|
||||
|
||||
std::unique_ptr<interfaces::Rpc> rpc{node_init->makeRpc()};
|
||||
assert(rpc);
|
||||
UniValue request{rh->PrepareRequest(strMethod, args)};
|
||||
UniValue reply{rpc->executeRpc(std::move(request), endpoint, username)};
|
||||
return rh->ProcessReply(reply);
|
||||
}
|
||||
|
||||
static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector<std::string>& args, const std::string& endpoint, const std::string& username)
|
||||
{
|
||||
std::string host;
|
||||
// In preference order, we choose the following for the port:
|
||||
|
@ -856,7 +893,7 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
|
|||
failedToGetAuthCookie = true;
|
||||
}
|
||||
} else {
|
||||
strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", "");
|
||||
strRPCUserColonPass = username + ":" + gArgs.GetArg("-rpcpassword", "");
|
||||
}
|
||||
|
||||
struct evkeyvalq* output_headers = evhttp_request_get_output_headers(req.get());
|
||||
|
@ -872,17 +909,7 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
|
|||
assert(output_buffer);
|
||||
evbuffer_add(output_buffer, strRequest.data(), strRequest.size());
|
||||
|
||||
// check if we should use a special wallet endpoint
|
||||
std::string endpoint = "/";
|
||||
if (rpcwallet) {
|
||||
char* encodedURI = evhttp_uriencode(rpcwallet->data(), rpcwallet->size(), false);
|
||||
if (encodedURI) {
|
||||
endpoint = "/wallet/" + std::string(encodedURI);
|
||||
free(encodedURI);
|
||||
} else {
|
||||
throw CConnectionFailed("uri-encode failed");
|
||||
}
|
||||
}
|
||||
|
||||
int r = evhttp_make_request(evcon.get(), req.get(), EVHTTP_REQ_POST, endpoint.c_str());
|
||||
req.release(); // ownership moved to evcon in above call
|
||||
if (r != 0) {
|
||||
|
@ -943,9 +970,26 @@ static UniValue ConnectAndCallRPC(BaseRequestHandler* rh, const std::string& str
|
|||
const int timeout = gArgs.GetIntArg("-rpcwaittimeout", DEFAULT_WAIT_CLIENT_TIMEOUT);
|
||||
const auto deadline{std::chrono::steady_clock::now() + 1s * timeout};
|
||||
|
||||
// check if we should use a special wallet endpoint
|
||||
std::string endpoint = "/";
|
||||
if (rpcwallet) {
|
||||
char* encodedURI = evhttp_uriencode(rpcwallet->data(), rpcwallet->size(), false);
|
||||
if (encodedURI) {
|
||||
endpoint = "/wallet/" + std::string(encodedURI);
|
||||
free(encodedURI);
|
||||
} else {
|
||||
throw CConnectionFailed("uri-encode failed");
|
||||
}
|
||||
}
|
||||
|
||||
std::string username{gArgs.GetArg("-rpcuser", "")};
|
||||
if (auto response{CallIPC(rh, strMethod, args, endpoint, username)}) {
|
||||
return *response;
|
||||
}
|
||||
|
||||
do {
|
||||
try {
|
||||
response = CallRPC(rh, strMethod, args, rpcwallet);
|
||||
response = CallRPC(rh, strMethod, args, endpoint, username);
|
||||
if (fWait) {
|
||||
const UniValue& error = response.find_value("error");
|
||||
if (!error.isNull() && error["code"].getInt<int>() == RPC_IN_WARMUP) {
|
||||
|
|
143
src/httprpc.cpp
143
src/httprpc.cpp
|
@ -79,13 +79,15 @@ static std::vector<std::vector<std::string>> g_rpcauth;
|
|||
static std::map<std::string, std::set<std::string>> g_rpc_whitelist;
|
||||
static bool g_rpc_whitelist_default = false;
|
||||
|
||||
static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCRequest& jreq)
|
||||
static UniValue JSONErrorReply(UniValue objError, const JSONRPCRequest& jreq, HTTPStatusCode& nStatus)
|
||||
{
|
||||
// Sending HTTP errors is a legacy JSON-RPC behavior.
|
||||
// HTTP errors should never be returned if JSON-RPC v2 was requested. This
|
||||
// function should only be called when a v1 request fails or when a request
|
||||
// cannot be parsed, so the version is unknown.
|
||||
Assume(jreq.m_json_version != JSONRPCVersion::V2);
|
||||
|
||||
// Send error reply from json-rpc error object
|
||||
int nStatus = HTTP_INTERNAL_SERVER_ERROR;
|
||||
nStatus = HTTP_INTERNAL_SERVER_ERROR;
|
||||
int code = objError.find_value("code").getInt<int>();
|
||||
|
||||
if (code == RPC_INVALID_REQUEST)
|
||||
|
@ -93,10 +95,7 @@ static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCReq
|
|||
else if (code == RPC_METHOD_NOT_FOUND)
|
||||
nStatus = HTTP_NOT_FOUND;
|
||||
|
||||
std::string strReply = JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version).write() + "\n";
|
||||
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(nStatus, strReply);
|
||||
return JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version);
|
||||
}
|
||||
|
||||
//This function checks username and password against -rpcauth
|
||||
|
@ -153,60 +152,23 @@ static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUserna
|
|||
return multiUserAuthorized(strUserPass);
|
||||
}
|
||||
|
||||
static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
|
||||
UniValue ExecuteHTTPRPC(const UniValue& valRequest, JSONRPCRequest& jreq, HTTPStatusCode& status)
|
||||
{
|
||||
// JSONRPC handles only POST
|
||||
if (req->GetRequestMethod() != HTTPRequest::POST) {
|
||||
req->WriteReply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests");
|
||||
return false;
|
||||
}
|
||||
// Check authorization
|
||||
std::pair<bool, std::string> authHeader = req->GetHeader("authorization");
|
||||
if (!authHeader.first) {
|
||||
req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
|
||||
req->WriteReply(HTTP_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONRPCRequest jreq;
|
||||
jreq.context = context;
|
||||
jreq.peerAddr = req->GetPeer().ToStringAddrPort();
|
||||
if (!RPCAuthorized(authHeader.second, jreq.authUser)) {
|
||||
LogPrintf("ThreadRPCServer incorrect password attempt from %s\n", jreq.peerAddr);
|
||||
|
||||
/* Deter brute-forcing
|
||||
If this results in a DoS the user really
|
||||
shouldn't have their RPC port exposed. */
|
||||
UninterruptibleSleep(std::chrono::milliseconds{250});
|
||||
|
||||
req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
|
||||
req->WriteReply(HTTP_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
status = HTTP_OK;
|
||||
try {
|
||||
// Parse request
|
||||
UniValue valRequest;
|
||||
if (!valRequest.read(req->ReadBody()))
|
||||
throw JSONRPCError(RPC_PARSE_ERROR, "Parse error");
|
||||
|
||||
// Set the URI
|
||||
jreq.URI = req->GetURI();
|
||||
|
||||
UniValue reply;
|
||||
bool user_has_whitelist = g_rpc_whitelist.count(jreq.authUser);
|
||||
if (!user_has_whitelist && g_rpc_whitelist_default) {
|
||||
LogPrintf("RPC User %s not allowed to call any methods\n", jreq.authUser);
|
||||
req->WriteReply(HTTP_FORBIDDEN);
|
||||
return false;
|
||||
status = HTTP_FORBIDDEN;
|
||||
return {};
|
||||
|
||||
// singleton request
|
||||
} else if (valRequest.isObject()) {
|
||||
jreq.parse(valRequest);
|
||||
if (user_has_whitelist && !g_rpc_whitelist[jreq.authUser].count(jreq.strMethod)) {
|
||||
LogPrintf("RPC User %s not allowed to call method %s\n", jreq.authUser, jreq.strMethod);
|
||||
req->WriteReply(HTTP_FORBIDDEN);
|
||||
return false;
|
||||
status = HTTP_FORBIDDEN;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Legacy 1.0/1.1 behavior is for failed requests to throw
|
||||
|
@ -214,14 +176,13 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
|
|||
// 2.0 behavior is to catch exceptions and return HTTP success with
|
||||
// RPC errors, as long as there is not an actual HTTP server error.
|
||||
const bool catch_errors{jreq.m_json_version == JSONRPCVersion::V2};
|
||||
reply = JSONRPCExec(jreq, catch_errors);
|
||||
|
||||
UniValue reply{JSONRPCExec(jreq, catch_errors)};
|
||||
if (jreq.IsNotification()) {
|
||||
// Even though we do execute notifications, we do not respond to them
|
||||
req->WriteReply(HTTP_NO_CONTENT);
|
||||
return true;
|
||||
status = HTTP_NO_CONTENT;
|
||||
return {};
|
||||
}
|
||||
|
||||
return reply;
|
||||
// array of requests
|
||||
} else if (valRequest.isArray()) {
|
||||
// Check authorization for each request's method
|
||||
|
@ -235,15 +196,15 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
|
|||
std::string strMethod = request.find_value("method").get_str();
|
||||
if (!g_rpc_whitelist[jreq.authUser].count(strMethod)) {
|
||||
LogPrintf("RPC User %s not allowed to call method %s\n", jreq.authUser, strMethod);
|
||||
req->WriteReply(HTTP_FORBIDDEN);
|
||||
return false;
|
||||
status = HTTP_FORBIDDEN;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute each request
|
||||
reply = UniValue::VARR;
|
||||
UniValue reply = UniValue::VARR;
|
||||
for (size_t i{0}; i < valRequest.size(); ++i) {
|
||||
// Batches never throw HTTP errors, they are always just included
|
||||
// in "HTTP OK" responses. Notifications never get any response.
|
||||
|
@ -270,23 +231,71 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
|
|||
// empty response in this case to favor being backwards compatible
|
||||
// over complying with the JSON-RPC 2.0 spec in this case.
|
||||
if (reply.size() == 0 && valRequest.size() > 0) {
|
||||
req->WriteReply(HTTP_NO_CONTENT);
|
||||
return true;
|
||||
status = HTTP_NO_CONTENT;
|
||||
return {};
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
else
|
||||
throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");
|
||||
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(HTTP_OK, reply.write() + "\n");
|
||||
} catch (UniValue& e) {
|
||||
JSONErrorReply(req, std::move(e), jreq);
|
||||
return false;
|
||||
return JSONErrorReply(std::move(e), jreq, status);
|
||||
} catch (const std::exception& e) {
|
||||
JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq);
|
||||
return JSONErrorReply(JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq, status);
|
||||
}
|
||||
}
|
||||
|
||||
static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
|
||||
{
|
||||
// JSONRPC handles only POST
|
||||
if (req->GetRequestMethod() != HTTPRequest::POST) {
|
||||
req->WriteReply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// Check authorization
|
||||
std::pair<bool, std::string> authHeader = req->GetHeader("authorization");
|
||||
if (!authHeader.first) {
|
||||
req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
|
||||
req->WriteReply(HTTP_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
JSONRPCRequest jreq;
|
||||
jreq.context = context;
|
||||
jreq.peerAddr = req->GetPeer().ToStringAddrPort();
|
||||
jreq.URI = req->GetURI();
|
||||
if (!RPCAuthorized(authHeader.second, jreq.authUser)) {
|
||||
LogPrintf("ThreadRPCServer incorrect password attempt from %s\n", jreq.peerAddr);
|
||||
|
||||
/* Deter brute-forcing
|
||||
If this results in a DoS the user really
|
||||
shouldn't have their RPC port exposed. */
|
||||
UninterruptibleSleep(std::chrono::milliseconds{250});
|
||||
|
||||
req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
|
||||
req->WriteReply(HTTP_UNAUTHORIZED);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate reply
|
||||
HTTPStatusCode status;
|
||||
UniValue reply;
|
||||
UniValue request;
|
||||
if (request.read(req->ReadBody())) {
|
||||
reply = ExecuteHTTPRPC(request, jreq, status);
|
||||
} else {
|
||||
reply = JSONErrorReply(JSONRPCError(RPC_PARSE_ERROR, "Parse error"), jreq, status);
|
||||
}
|
||||
|
||||
// Write reply
|
||||
if (reply.isNull()) {
|
||||
// Error case or no-content notification reply.
|
||||
req->WriteReply(status);
|
||||
} else {
|
||||
req->WriteHeader("Content-Type", "application/json");
|
||||
req->WriteReply(status, reply.write() + "\n");
|
||||
}
|
||||
return status < 400;
|
||||
}
|
||||
|
||||
static bool InitRPCAuthentication()
|
||||
|
|
|
@ -7,6 +7,10 @@
|
|||
|
||||
#include <any>
|
||||
|
||||
class JSONRPCRequest;
|
||||
class UniValue;
|
||||
enum HTTPStatusCode : int;
|
||||
|
||||
/** Start HTTP RPC subsystem.
|
||||
* Precondition; HTTP and RPC has been started.
|
||||
*/
|
||||
|
@ -19,6 +23,9 @@ void InterruptHTTPRPC();
|
|||
*/
|
||||
void StopHTTPRPC();
|
||||
|
||||
/** Execute a single HTTP request, containing one or more JSONRPC requests. */
|
||||
UniValue ExecuteHTTPRPC(const UniValue& valRequest, JSONRPCRequest& jreq, HTTPStatusCode& status);
|
||||
|
||||
/** Start HTTP REST subsystem.
|
||||
* Precondition; HTTP and RPC has been started.
|
||||
*/
|
||||
|
|
25
src/init/basic-ipc.cpp
Normal file
25
src/init/basic-ipc.cpp
Normal file
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) 2025 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <interfaces/init.h>
|
||||
#include <interfaces/ipc.h>
|
||||
|
||||
namespace init {
|
||||
namespace {
|
||||
class BitcoinBasicInit : public interfaces::Init
|
||||
{
|
||||
public:
|
||||
BitcoinBasicInit(const char* exe_name, const char* process_argv0) : m_ipc(interfaces::MakeIpc(exe_name, process_argv0, *this)) {}
|
||||
interfaces::Ipc* ipc() override { return m_ipc.get(); }
|
||||
std::unique_ptr<interfaces::Ipc> m_ipc;
|
||||
};
|
||||
} // namespace
|
||||
} // namespace init
|
||||
|
||||
namespace interfaces {
|
||||
std::unique_ptr<Init> MakeBasicInit(const char* exe_name, const char* process_argv0)
|
||||
{
|
||||
return std::make_unique<init::BitcoinBasicInit>(exe_name, process_argv0);
|
||||
}
|
||||
} // namespace interfaces
|
12
src/init/basic.cpp
Normal file
12
src/init/basic.cpp
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2025 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#include <interfaces/init.h>
|
||||
|
||||
namespace interfaces {
|
||||
std::unique_ptr<Init> MakeBasicInit(const char* exe_name, const char* process_argv0)
|
||||
{
|
||||
return std::make_unique<Init>();
|
||||
}
|
||||
} // namespace interfaces
|
|
@ -8,6 +8,7 @@
|
|||
#include <interfaces/init.h>
|
||||
#include <interfaces/ipc.h>
|
||||
#include <interfaces/node.h>
|
||||
#include <interfaces/rpc.h>
|
||||
#include <interfaces/wallet.h>
|
||||
#include <node/context.h>
|
||||
#include <util/check.h>
|
||||
|
@ -33,6 +34,7 @@ public:
|
|||
return MakeWalletLoader(chain, *Assert(m_node.args));
|
||||
}
|
||||
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
|
||||
std::unique_ptr<interfaces::Rpc> makeRpc() override { return interfaces::MakeRpc(m_node); }
|
||||
interfaces::Ipc* ipc() override { return m_ipc.get(); }
|
||||
// bitcoin-gui accepts -ipcbind option even though it does not use it
|
||||
// directly. It just returns true here to accept the option because
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include <interfaces/init.h>
|
||||
#include <interfaces/ipc.h>
|
||||
#include <interfaces/node.h>
|
||||
#include <interfaces/rpc.h>
|
||||
#include <interfaces/wallet.h>
|
||||
#include <node/context.h>
|
||||
#include <util/check.h>
|
||||
|
@ -36,6 +37,7 @@ public:
|
|||
return MakeWalletLoader(chain, *Assert(m_node.args));
|
||||
}
|
||||
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
|
||||
std::unique_ptr<interfaces::Rpc> makeRpc() override { return interfaces::MakeRpc(m_node); }
|
||||
interfaces::Ipc* ipc() override { return m_ipc.get(); }
|
||||
bool canListenIpc() override { return true; }
|
||||
node::NodeContext& m_node;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <interfaces/echo.h>
|
||||
#include <interfaces/mining.h>
|
||||
#include <interfaces/node.h>
|
||||
#include <interfaces/rpc.h>
|
||||
#include <interfaces/wallet.h>
|
||||
|
||||
#include <memory>
|
||||
|
@ -36,6 +37,7 @@ public:
|
|||
virtual std::unique_ptr<Mining> makeMining() { return nullptr; }
|
||||
virtual std::unique_ptr<WalletLoader> makeWalletLoader(Chain& chain) { return nullptr; }
|
||||
virtual std::unique_ptr<Echo> makeEcho() { return nullptr; }
|
||||
virtual std::unique_ptr<Rpc> makeRpc() { return nullptr; }
|
||||
virtual Ipc* ipc() { return nullptr; }
|
||||
virtual bool canListenIpc() { return false; }
|
||||
};
|
||||
|
@ -53,6 +55,25 @@ std::unique_ptr<Init> MakeWalletInit(int argc, char* argv[], int& exit_status);
|
|||
|
||||
//! Return implementation of Init interface for the gui process.
|
||||
std::unique_ptr<Init> MakeGuiInit(int argc, char* argv[]);
|
||||
|
||||
//! Return implementation of Init interface for a basic IPC client that doesn't
|
||||
//! provide any IPC services itself.
|
||||
//!
|
||||
//! When an IPC client connects to a socket or spawns a process, it gets a pointer
|
||||
//! to an Init object allowing it to create objects and threads on the remote
|
||||
//! side of the IPC connection. But the client also needs to provide a local Init
|
||||
//! object to allow the remote side of the connection to create objects and
|
||||
//! threads on this side. This function just returns a basic Init object
|
||||
//! allowing remote connections to only create local threads, not other objects
|
||||
//! (because its Init::make* methods return null.)
|
||||
//!
|
||||
//! @param exe_name Current executable name, which is just passed to the IPC
|
||||
//! system and used for logging.
|
||||
//!
|
||||
//! @param process_argv0 Optional string containing argv[0] value passed to
|
||||
//! main(). This is passed to the IPC system and used to locate binaries by
|
||||
//! relative path if subprocesses are spawned.
|
||||
std::unique_ptr<Init> MakeBasicInit(const char* exe_name, const char* process_argv0="");
|
||||
} // namespace interfaces
|
||||
|
||||
#endif // BITCOIN_INTERFACES_INIT_H
|
||||
|
|
31
src/interfaces/rpc.h
Normal file
31
src/interfaces/rpc.h
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright (c) 2025 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_INTERFACES_RPC_H
|
||||
#define BITCOIN_INTERFACES_RPC_H
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class UniValue;
|
||||
|
||||
namespace node {
|
||||
struct NodeContext;
|
||||
} // namespace node
|
||||
|
||||
namespace interfaces {
|
||||
//! Interface giving clients ability to emulate RPC calls.
|
||||
class Rpc
|
||||
{
|
||||
public:
|
||||
virtual ~Rpc() = default;
|
||||
virtual UniValue executeRpc(UniValue request, std::string url, std::string user) = 0;
|
||||
};
|
||||
|
||||
//! Return implementation of Rpc interface.
|
||||
std::unique_ptr<Rpc> MakeRpc(node::NodeContext& node);
|
||||
|
||||
} // namespace interfaces
|
||||
|
||||
#endif // BITCOIN_INTERFACES_RPC_H
|
|
@ -14,6 +14,7 @@ target_capnp_sources(bitcoin_ipc ${PROJECT_SOURCE_DIR}
|
|||
capnp/echo.capnp
|
||||
capnp/init.capnp
|
||||
capnp/mining.capnp
|
||||
capnp/rpc.capnp
|
||||
)
|
||||
|
||||
target_link_libraries(bitcoin_ipc
|
||||
|
|
|
@ -7,5 +7,6 @@
|
|||
|
||||
#include <ipc/capnp/echo.capnp.proxy-types.h>
|
||||
#include <ipc/capnp/mining.capnp.proxy-types.h>
|
||||
#include <ipc/capnp/rpc.capnp.proxy-types.h>
|
||||
|
||||
#endif // BITCOIN_IPC_CAPNP_INIT_TYPES_H
|
||||
|
|
|
@ -15,9 +15,11 @@ $Proxy.includeTypes("ipc/capnp/init-types.h");
|
|||
|
||||
using Echo = import "echo.capnp";
|
||||
using Mining = import "mining.capnp";
|
||||
using Rpc = import "rpc.capnp";
|
||||
|
||||
interface Init $Proxy.wrap("interfaces::Init") {
|
||||
construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap);
|
||||
makeEcho @1 (context :Proxy.Context) -> (result :Echo.Echo);
|
||||
makeMining @2 (context :Proxy.Context) -> (result :Mining.Mining);
|
||||
makeRpc @3 (context :Proxy.Context) -> (result :Rpc.Rpc);
|
||||
}
|
||||
|
|
12
src/ipc/capnp/rpc-types.h
Normal file
12
src/ipc/capnp/rpc-types.h
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Copyright (c) 2025 The Bitcoin Core developers
|
||||
// Distributed under the MIT software license, see the accompanying
|
||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
#ifndef BITCOIN_IPC_CAPNP_RPC_TYPES_H
|
||||
#define BITCOIN_IPC_CAPNP_RPC_TYPES_H
|
||||
|
||||
#include <ipc/capnp/common.capnp.proxy-types.h>
|
||||
#include <ipc/capnp/common-types.h>
|
||||
#include <ipc/capnp/rpc.capnp.proxy.h>
|
||||
|
||||
#endif // BITCOIN_IPC_CAPNP_RPC_TYPES_H
|
17
src/ipc/capnp/rpc.capnp
Normal file
17
src/ipc/capnp/rpc.capnp
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Copyright (c) 2025 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
|
||||
@0x9c3505dc45e146ac;
|
||||
|
||||
using Cxx = import "/capnp/c++.capnp";
|
||||
$Cxx.namespace("ipc::capnp::messages");
|
||||
|
||||
using Common = import "common.capnp";
|
||||
using Proxy = import "/mp/proxy.capnp";
|
||||
$Proxy.include("interfaces/rpc.h");
|
||||
$Proxy.includeTypes("ipc/capnp/rpc-types.h");
|
||||
|
||||
interface Rpc $Proxy.wrap("interfaces::Rpc") {
|
||||
executeRpc @0 (context :Proxy.Context, request :Text, uri :Text, user :Text) -> (result :Text);
|
||||
}
|
|
@ -71,10 +71,13 @@ public:
|
|||
fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address);
|
||||
} catch (const std::system_error& e) {
|
||||
// If connection type is auto and socket path isn't accepting connections, or doesn't exist, catch the error and return null;
|
||||
if (e.code() == std::errc::connection_refused || e.code() == std::errc::no_such_file_or_directory) {
|
||||
if (e.code() == std::errc::connection_refused || e.code() == std::errc::no_such_file_or_directory || e.code() == std::errc::not_a_directory) {
|
||||
return nullptr;
|
||||
}
|
||||
throw;
|
||||
} catch (const std::invalid_argument&) {
|
||||
// Catch 'Unix address path "..." exceeded maximum socket path length' error
|
||||
return nullptr;
|
||||
}
|
||||
} else {
|
||||
fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address);
|
||||
|
|
|
@ -12,12 +12,14 @@
|
|||
#include <consensus/validation.h>
|
||||
#include <deploymentstatus.h>
|
||||
#include <external_signer.h>
|
||||
#include <httprpc.h>
|
||||
#include <index/blockfilterindex.h>
|
||||
#include <init.h>
|
||||
#include <interfaces/chain.h>
|
||||
#include <interfaces/handler.h>
|
||||
#include <interfaces/mining.h>
|
||||
#include <interfaces/node.h>
|
||||
#include <interfaces/rpc.h>
|
||||
#include <interfaces/types.h>
|
||||
#include <interfaces/wallet.h>
|
||||
#include <kernel/chain.h>
|
||||
|
@ -80,6 +82,7 @@ using interfaces::Handler;
|
|||
using interfaces::MakeSignalHandler;
|
||||
using interfaces::Mining;
|
||||
using interfaces::Node;
|
||||
using interfaces::Rpc;
|
||||
using interfaces::WalletLoader;
|
||||
using node::BlockAssembler;
|
||||
using node::BlockWaitOptions;
|
||||
|
@ -1115,6 +1118,24 @@ public:
|
|||
KernelNotifications& notifications() { return *Assert(m_node.notifications); }
|
||||
NodeContext& m_node;
|
||||
};
|
||||
|
||||
class RpcImpl : public Rpc
|
||||
{
|
||||
public:
|
||||
explicit RpcImpl(NodeContext& node) : m_node(node) {}
|
||||
|
||||
UniValue executeRpc(UniValue request, std::string uri, std::string user) override
|
||||
{
|
||||
JSONRPCRequest req;
|
||||
req.context = &m_node;
|
||||
req.URI = std::move(uri);
|
||||
req.authUser = std::move(user);
|
||||
HTTPStatusCode status;
|
||||
return ExecuteHTTPRPC(request, req, status);
|
||||
}
|
||||
|
||||
NodeContext& m_node;
|
||||
};
|
||||
} // namespace
|
||||
} // namespace node
|
||||
|
||||
|
@ -1122,4 +1143,5 @@ namespace interfaces {
|
|||
std::unique_ptr<Node> MakeNode(node::NodeContext& context) { return std::make_unique<node::NodeImpl>(context); }
|
||||
std::unique_ptr<Chain> MakeChain(node::NodeContext& context) { return std::make_unique<node::ChainImpl>(context); }
|
||||
std::unique_ptr<Mining> MakeMining(node::NodeContext& context) { return std::make_unique<node::MinerImpl>(context); }
|
||||
std::unique_ptr<Rpc> MakeRpc(node::NodeContext& context) { return std::make_unique<node::RpcImpl>(context); }
|
||||
} // namespace interfaces
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
#define BITCOIN_RPC_PROTOCOL_H
|
||||
|
||||
//! HTTP status codes
|
||||
enum HTTPStatusCode
|
||||
enum HTTPStatusCode : int
|
||||
{
|
||||
HTTP_OK = 200,
|
||||
HTTP_NO_CONTENT = 204,
|
||||
|
|
|
@ -26,6 +26,7 @@ function(create_test_config)
|
|||
set_configure_variable(WITH_ZMQ ENABLE_ZMQ)
|
||||
set_configure_variable(ENABLE_EXTERNAL_SIGNER ENABLE_EXTERNAL_SIGNER)
|
||||
set_configure_variable(WITH_USDT ENABLE_USDT_TRACEPOINTS)
|
||||
set_configure_variable(ENABLE_IPC ENABLE_IPC)
|
||||
|
||||
configure_file(config.ini.in config.ini USE_SOURCE_PERMISSIONS @ONLY)
|
||||
endfunction()
|
||||
|
|
|
@ -26,3 +26,4 @@ RPCAUTH=@abs_top_srcdir@/share/rpcauth/rpcauth.py
|
|||
@ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true
|
||||
@ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true
|
||||
@ENABLE_USDT_TRACEPOINTS_TRUE@ENABLE_USDT_TRACEPOINTS=true
|
||||
@ENABLE_IPC_TRUE@ENABLE_IPC=true
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
from decimal import Decimal
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from test_framework.blocktools import COINBASE_MATURITY
|
||||
from test_framework.netutil import test_ipv6_local
|
||||
|
@ -398,6 +399,14 @@ class TestBitcoinCli(BitcoinTestFramework):
|
|||
self.log.info("Test that only one of -addrinfo, -generate, -getinfo, -netinfo may be specified at a time")
|
||||
assert_raises_process_error(1, "Only one of -getinfo, -netinfo may be specified", self.nodes[0].cli('-getinfo', '-netinfo').send_cli)
|
||||
|
||||
if not self.is_ipc_enabled():
|
||||
self.log.info("Test bitcoin-cli -ipcconnect triggers error if not built with IPC support")
|
||||
args = [self.binary_paths.bitcoincli, "-ipcconnect=unix", "-getinfo"]
|
||||
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
assert_equal(result.stdout, "error: bitcoin-cli was not built with IPC support\n")
|
||||
assert_equal(result.stderr, None)
|
||||
assert_equal(result.returncode, 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
TestBitcoinCli(__file__).main()
|
||||
|
|
81
test/functional/interface_ipc_cli.py
Executable file
81
test/functional/interface_ipc_cli.py
Executable file
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2025 The Bitcoin Core developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||
"""Test IPC with bitcoin-cli"""
|
||||
|
||||
from test_framework.test_framework import BitcoinTestFramework
|
||||
from test_framework.util import (
|
||||
assert_equal,
|
||||
rpc_port
|
||||
)
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
class TestBitcoinIpcCli(BitcoinTestFramework):
|
||||
def set_test_params(self):
|
||||
self.setup_clean_chain = True
|
||||
self.num_nodes = 1
|
||||
|
||||
def skip_test_if_missing_module(self):
|
||||
self.skip_if_no_ipc()
|
||||
|
||||
def setup_nodes(self):
|
||||
# Always run IPC node binary
|
||||
self.binary_paths.bitcoind = self.binary_paths.bitcoin_node
|
||||
|
||||
# Work around default CI path exceeding maximum socket path length.
|
||||
# On Linux sun_path is 108 bytes, on macOS it's only 104. Includes
|
||||
# null terminator.
|
||||
self.socket_path = self.options.tmpdir + "/node0/regtest/node.sock"
|
||||
if len(self.socket_path.encode('utf-8')) < 104:
|
||||
self.extra_args = [["-ipcbind=unix"]]
|
||||
self.default_socket = True
|
||||
else:
|
||||
self.socket_path = tempfile.mktemp()
|
||||
self.extra_args = [[f"-ipcbind=unix:{self.socket_path}"]]
|
||||
self.default_socket = False
|
||||
super().setup_nodes()
|
||||
|
||||
def test_cli(self, args, error=None):
|
||||
# Intentionally set wrong RPC password so only IPC not HTTP connections work
|
||||
args = [self.binary_paths.bitcoincli, f"-datadir={self.nodes[0].datadir_path}", "-rpcpassword=wrong"] + args + ["echo", "foo"]
|
||||
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
if error:
|
||||
assert_equal(result.stdout, error)
|
||||
else:
|
||||
assert_equal(result.stdout, '[\n "foo"\n]\n')
|
||||
assert_equal(result.stderr, None)
|
||||
assert_equal(result.returncode, 1 if error else 0)
|
||||
|
||||
def run_test(self):
|
||||
if not self.default_socket:
|
||||
self.log.info("Skipping a few checks because temporary directory path is too long")
|
||||
|
||||
http_auth_error = "error: Authorization failed: Incorrect rpcuser or rpcpassword\n"
|
||||
http_connect_error = f"error: timeout on transient error: Could not connect to the server 127.0.0.1:{rpc_port(self.nodes[0].index)}\n\nMake sure the bitcoind server is running and that you are connecting to the correct RPC port.\nUse \"bitcoin-cli -help\" for more info.\n"
|
||||
http_ipc_error = "error: -rpcconnect and -ipcconnect options cannot both be enabled\n"
|
||||
ipc_connect_error = "error: Connection refused\n\nProbably bitcoin-node is not running or not listening on a unix socket. Can be started with:\n\n bitcoin-node -chain=regtest -ipcbind=unix\n"
|
||||
|
||||
for started in (True, False):
|
||||
auto_error = None if started else http_connect_error
|
||||
http_error = http_auth_error if started else http_connect_error
|
||||
ipc_error = None if started else ipc_connect_error
|
||||
|
||||
if self.default_socket:
|
||||
self.test_cli([], auto_error)
|
||||
self.test_cli(["-rpcconnect=127.0.0.1"], http_error)
|
||||
self.test_cli(["-ipcconnect=auto"], auto_error)
|
||||
self.test_cli(["-ipcconnect=auto", "-rpcconnect=127.0.0.1"], http_error)
|
||||
self.test_cli(["-ipcconnect=unix"], ipc_error)
|
||||
|
||||
self.test_cli([f"-ipcconnect=unix:{self.socket_path}"], ipc_error)
|
||||
self.test_cli(["-noipcconnect"], http_error)
|
||||
self.test_cli(["-ipcconnect=unix", "-rpcconnect=127.0.0.1"], http_ipc_error)
|
||||
|
||||
self.stop_node(0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
TestBitcoinIpcCli(__file__).main()
|
|
@ -285,13 +285,15 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||
"bitcoin-chainstate": ("bitcoinchainstate", "BITCOINCHAINSTATE"),
|
||||
"bitcoin-wallet": ("bitcoinwallet", "BITCOINWALLET"),
|
||||
}
|
||||
for binary, [attribute_name, env_variable_name] in binaries.items():
|
||||
default_filename = os.path.join(
|
||||
def binary_path(binary):
|
||||
return os.path.join(
|
||||
self.config["environment"]["BUILDDIR"],
|
||||
"bin",
|
||||
binary + self.config["environment"]["EXEEXT"],
|
||||
)
|
||||
setattr(paths, attribute_name, os.getenv(env_variable_name, default=default_filename))
|
||||
for binary, [attribute_name, env_variable_name] in binaries.items():
|
||||
setattr(paths, attribute_name, os.getenv(env_variable_name) or binary_path(binary))
|
||||
paths.bitcoin_node = binary_path("bitcoin-node")
|
||||
return paths
|
||||
|
||||
def get_binaries(self, bin_dir=None):
|
||||
|
@ -1007,6 +1009,11 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||
if not self.is_cli_compiled():
|
||||
raise SkipTest("bitcoin-cli has not been compiled.")
|
||||
|
||||
def skip_if_no_ipc(self):
|
||||
"""Skip the running test if ipc is not enabled."""
|
||||
if not self.is_ipc_enabled():
|
||||
raise SkipTest("ipc is not enabled.")
|
||||
|
||||
def skip_if_no_previous_releases(self):
|
||||
"""Skip the running test if previous releases are not available."""
|
||||
if not self.has_previous_releases():
|
||||
|
@ -1061,5 +1068,9 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
|||
"""Checks whether the wallet module was compiled with BDB support."""
|
||||
return self.config["components"].getboolean("USE_BDB")
|
||||
|
||||
def is_ipc_enabled(self):
|
||||
"""Checks whether ipc is enabled."""
|
||||
return self.config["components"].getboolean("ENABLE_IPC")
|
||||
|
||||
def has_blockfile(self, node, filenum: str):
|
||||
return (node.blocks_path/ f"blk{filenum}.dat").is_file()
|
||||
|
|
|
@ -355,6 +355,7 @@ BASE_SCRIPTS = [
|
|||
'rpc_help.py',
|
||||
'p2p_handshake.py',
|
||||
'p2p_handshake.py --v2transport',
|
||||
'interface_ipc_cli.py',
|
||||
'feature_dirsymlinks.py',
|
||||
'feature_help.py',
|
||||
'feature_shutdown.py',
|
||||
|
|
Loading…
Add table
Reference in a new issue