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
|
# Bitcoin Core RPC client
|
||||||
if(BUILD_CLI)
|
if(BUILD_CLI)
|
||||||
add_executable(bitcoin-cli bitcoin-cli.cpp)
|
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)
|
add_windows_resources(bitcoin-cli bitcoin-cli-res.rc)
|
||||||
target_link_libraries(bitcoin-cli
|
target_link_libraries(bitcoin-cli
|
||||||
core_interface
|
core_interface
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
#include <common/system.h>
|
#include <common/system.h>
|
||||||
#include <compat/compat.h>
|
#include <compat/compat.h>
|
||||||
#include <compat/stdin.h>
|
#include <compat/stdin.h>
|
||||||
|
#include <interfaces/init.h>
|
||||||
|
#include <interfaces/ipc.h>
|
||||||
|
#include <interfaces/rpc.h>
|
||||||
#include <policy/feerate.h>
|
#include <policy/feerate.h>
|
||||||
#include <rpc/client.h>
|
#include <rpc/client.h>
|
||||||
#include <rpc/mining.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("-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("-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("-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)
|
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;
|
std::string host;
|
||||||
// In preference order, we choose the following for the port:
|
// 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;
|
failedToGetAuthCookie = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", "");
|
strRPCUserColonPass = username + ":" + gArgs.GetArg("-rpcpassword", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
struct evkeyvalq* output_headers = evhttp_request_get_output_headers(req.get());
|
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);
|
assert(output_buffer);
|
||||||
evbuffer_add(output_buffer, strRequest.data(), strRequest.size());
|
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());
|
int r = evhttp_make_request(evcon.get(), req.get(), EVHTTP_REQ_POST, endpoint.c_str());
|
||||||
req.release(); // ownership moved to evcon in above call
|
req.release(); // ownership moved to evcon in above call
|
||||||
if (r != 0) {
|
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 int timeout = gArgs.GetIntArg("-rpcwaittimeout", DEFAULT_WAIT_CLIENT_TIMEOUT);
|
||||||
const auto deadline{std::chrono::steady_clock::now() + 1s * 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 {
|
do {
|
||||||
try {
|
try {
|
||||||
response = CallRPC(rh, strMethod, args, rpcwallet);
|
response = CallRPC(rh, strMethod, args, endpoint, username);
|
||||||
if (fWait) {
|
if (fWait) {
|
||||||
const UniValue& error = response.find_value("error");
|
const UniValue& error = response.find_value("error");
|
||||||
if (!error.isNull() && error["code"].getInt<int>() == RPC_IN_WARMUP) {
|
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 std::map<std::string, std::set<std::string>> g_rpc_whitelist;
|
||||||
static bool g_rpc_whitelist_default = false;
|
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);
|
Assume(jreq.m_json_version != JSONRPCVersion::V2);
|
||||||
|
|
||||||
// Send error reply from json-rpc error object
|
// 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>();
|
int code = objError.find_value("code").getInt<int>();
|
||||||
|
|
||||||
if (code == RPC_INVALID_REQUEST)
|
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)
|
else if (code == RPC_METHOD_NOT_FOUND)
|
||||||
nStatus = HTTP_NOT_FOUND;
|
nStatus = HTTP_NOT_FOUND;
|
||||||
|
|
||||||
std::string strReply = JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version).write() + "\n";
|
return JSONRPCReplyObj(NullUniValue, std::move(objError), jreq.id, jreq.m_json_version);
|
||||||
|
|
||||||
req->WriteHeader("Content-Type", "application/json");
|
|
||||||
req->WriteReply(nStatus, strReply);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//This function checks username and password against -rpcauth
|
//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);
|
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
|
status = HTTP_OK;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
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);
|
bool user_has_whitelist = g_rpc_whitelist.count(jreq.authUser);
|
||||||
if (!user_has_whitelist && g_rpc_whitelist_default) {
|
if (!user_has_whitelist && g_rpc_whitelist_default) {
|
||||||
LogPrintf("RPC User %s not allowed to call any methods\n", jreq.authUser);
|
LogPrintf("RPC User %s not allowed to call any methods\n", jreq.authUser);
|
||||||
req->WriteReply(HTTP_FORBIDDEN);
|
status = HTTP_FORBIDDEN;
|
||||||
return false;
|
return {};
|
||||||
|
|
||||||
// singleton request
|
// singleton request
|
||||||
} else if (valRequest.isObject()) {
|
} else if (valRequest.isObject()) {
|
||||||
jreq.parse(valRequest);
|
jreq.parse(valRequest);
|
||||||
if (user_has_whitelist && !g_rpc_whitelist[jreq.authUser].count(jreq.strMethod)) {
|
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);
|
LogPrintf("RPC User %s not allowed to call method %s\n", jreq.authUser, jreq.strMethod);
|
||||||
req->WriteReply(HTTP_FORBIDDEN);
|
status = HTTP_FORBIDDEN;
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy 1.0/1.1 behavior is for failed requests to throw
|
// 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
|
// 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.
|
// RPC errors, as long as there is not an actual HTTP server error.
|
||||||
const bool catch_errors{jreq.m_json_version == JSONRPCVersion::V2};
|
const bool catch_errors{jreq.m_json_version == JSONRPCVersion::V2};
|
||||||
reply = JSONRPCExec(jreq, catch_errors);
|
UniValue reply{JSONRPCExec(jreq, catch_errors)};
|
||||||
|
|
||||||
if (jreq.IsNotification()) {
|
if (jreq.IsNotification()) {
|
||||||
// Even though we do execute notifications, we do not respond to them
|
// Even though we do execute notifications, we do not respond to them
|
||||||
req->WriteReply(HTTP_NO_CONTENT);
|
status = HTTP_NO_CONTENT;
|
||||||
return true;
|
return {};
|
||||||
}
|
}
|
||||||
|
return reply;
|
||||||
// array of requests
|
// array of requests
|
||||||
} else if (valRequest.isArray()) {
|
} else if (valRequest.isArray()) {
|
||||||
// Check authorization for each request's method
|
// 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();
|
std::string strMethod = request.find_value("method").get_str();
|
||||||
if (!g_rpc_whitelist[jreq.authUser].count(strMethod)) {
|
if (!g_rpc_whitelist[jreq.authUser].count(strMethod)) {
|
||||||
LogPrintf("RPC User %s not allowed to call method %s\n", jreq.authUser, strMethod);
|
LogPrintf("RPC User %s not allowed to call method %s\n", jreq.authUser, strMethod);
|
||||||
req->WriteReply(HTTP_FORBIDDEN);
|
status = HTTP_FORBIDDEN;
|
||||||
return false;
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute each request
|
// Execute each request
|
||||||
reply = UniValue::VARR;
|
UniValue reply = UniValue::VARR;
|
||||||
for (size_t i{0}; i < valRequest.size(); ++i) {
|
for (size_t i{0}; i < valRequest.size(); ++i) {
|
||||||
// Batches never throw HTTP errors, they are always just included
|
// Batches never throw HTTP errors, they are always just included
|
||||||
// in "HTTP OK" responses. Notifications never get any response.
|
// 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
|
// empty response in this case to favor being backwards compatible
|
||||||
// over complying with the JSON-RPC 2.0 spec in this case.
|
// over complying with the JSON-RPC 2.0 spec in this case.
|
||||||
if (reply.size() == 0 && valRequest.size() > 0) {
|
if (reply.size() == 0 && valRequest.size() > 0) {
|
||||||
req->WriteReply(HTTP_NO_CONTENT);
|
status = HTTP_NO_CONTENT;
|
||||||
return true;
|
return {};
|
||||||
}
|
}
|
||||||
|
return reply;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");
|
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) {
|
} catch (UniValue& e) {
|
||||||
JSONErrorReply(req, std::move(e), jreq);
|
return JSONErrorReply(std::move(e), jreq, status);
|
||||||
return false;
|
|
||||||
} catch (const std::exception& e) {
|
} 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 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()
|
static bool InitRPCAuthentication()
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
|
|
||||||
#include <any>
|
#include <any>
|
||||||
|
|
||||||
|
class JSONRPCRequest;
|
||||||
|
class UniValue;
|
||||||
|
enum HTTPStatusCode : int;
|
||||||
|
|
||||||
/** Start HTTP RPC subsystem.
|
/** Start HTTP RPC subsystem.
|
||||||
* Precondition; HTTP and RPC has been started.
|
* Precondition; HTTP and RPC has been started.
|
||||||
*/
|
*/
|
||||||
|
@ -19,6 +23,9 @@ void InterruptHTTPRPC();
|
||||||
*/
|
*/
|
||||||
void StopHTTPRPC();
|
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.
|
/** Start HTTP REST subsystem.
|
||||||
* Precondition; HTTP and RPC has been started.
|
* 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/init.h>
|
||||||
#include <interfaces/ipc.h>
|
#include <interfaces/ipc.h>
|
||||||
#include <interfaces/node.h>
|
#include <interfaces/node.h>
|
||||||
|
#include <interfaces/rpc.h>
|
||||||
#include <interfaces/wallet.h>
|
#include <interfaces/wallet.h>
|
||||||
#include <node/context.h>
|
#include <node/context.h>
|
||||||
#include <util/check.h>
|
#include <util/check.h>
|
||||||
|
@ -33,6 +34,7 @@ public:
|
||||||
return MakeWalletLoader(chain, *Assert(m_node.args));
|
return MakeWalletLoader(chain, *Assert(m_node.args));
|
||||||
}
|
}
|
||||||
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
|
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(); }
|
interfaces::Ipc* ipc() override { return m_ipc.get(); }
|
||||||
// bitcoin-gui accepts -ipcbind option even though it does not use it
|
// bitcoin-gui accepts -ipcbind option even though it does not use it
|
||||||
// directly. It just returns true here to accept the option because
|
// directly. It just returns true here to accept the option because
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <interfaces/init.h>
|
#include <interfaces/init.h>
|
||||||
#include <interfaces/ipc.h>
|
#include <interfaces/ipc.h>
|
||||||
#include <interfaces/node.h>
|
#include <interfaces/node.h>
|
||||||
|
#include <interfaces/rpc.h>
|
||||||
#include <interfaces/wallet.h>
|
#include <interfaces/wallet.h>
|
||||||
#include <node/context.h>
|
#include <node/context.h>
|
||||||
#include <util/check.h>
|
#include <util/check.h>
|
||||||
|
@ -36,6 +37,7 @@ public:
|
||||||
return MakeWalletLoader(chain, *Assert(m_node.args));
|
return MakeWalletLoader(chain, *Assert(m_node.args));
|
||||||
}
|
}
|
||||||
std::unique_ptr<interfaces::Echo> makeEcho() override { return interfaces::MakeEcho(); }
|
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(); }
|
interfaces::Ipc* ipc() override { return m_ipc.get(); }
|
||||||
bool canListenIpc() override { return true; }
|
bool canListenIpc() override { return true; }
|
||||||
node::NodeContext& m_node;
|
node::NodeContext& m_node;
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <interfaces/echo.h>
|
#include <interfaces/echo.h>
|
||||||
#include <interfaces/mining.h>
|
#include <interfaces/mining.h>
|
||||||
#include <interfaces/node.h>
|
#include <interfaces/node.h>
|
||||||
|
#include <interfaces/rpc.h>
|
||||||
#include <interfaces/wallet.h>
|
#include <interfaces/wallet.h>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
@ -36,6 +37,7 @@ public:
|
||||||
virtual std::unique_ptr<Mining> makeMining() { return nullptr; }
|
virtual std::unique_ptr<Mining> makeMining() { return nullptr; }
|
||||||
virtual std::unique_ptr<WalletLoader> makeWalletLoader(Chain& chain) { return nullptr; }
|
virtual std::unique_ptr<WalletLoader> makeWalletLoader(Chain& chain) { return nullptr; }
|
||||||
virtual std::unique_ptr<Echo> makeEcho() { return nullptr; }
|
virtual std::unique_ptr<Echo> makeEcho() { return nullptr; }
|
||||||
|
virtual std::unique_ptr<Rpc> makeRpc() { return nullptr; }
|
||||||
virtual Ipc* ipc() { return nullptr; }
|
virtual Ipc* ipc() { return nullptr; }
|
||||||
virtual bool canListenIpc() { return false; }
|
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.
|
//! Return implementation of Init interface for the gui process.
|
||||||
std::unique_ptr<Init> MakeGuiInit(int argc, char* argv[]);
|
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
|
} // namespace interfaces
|
||||||
|
|
||||||
#endif // BITCOIN_INTERFACES_INIT_H
|
#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/echo.capnp
|
||||||
capnp/init.capnp
|
capnp/init.capnp
|
||||||
capnp/mining.capnp
|
capnp/mining.capnp
|
||||||
|
capnp/rpc.capnp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(bitcoin_ipc
|
target_link_libraries(bitcoin_ipc
|
||||||
|
|
|
@ -7,5 +7,6 @@
|
||||||
|
|
||||||
#include <ipc/capnp/echo.capnp.proxy-types.h>
|
#include <ipc/capnp/echo.capnp.proxy-types.h>
|
||||||
#include <ipc/capnp/mining.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
|
#endif // BITCOIN_IPC_CAPNP_INIT_TYPES_H
|
||||||
|
|
|
@ -15,9 +15,11 @@ $Proxy.includeTypes("ipc/capnp/init-types.h");
|
||||||
|
|
||||||
using Echo = import "echo.capnp";
|
using Echo = import "echo.capnp";
|
||||||
using Mining = import "mining.capnp";
|
using Mining = import "mining.capnp";
|
||||||
|
using Rpc = import "rpc.capnp";
|
||||||
|
|
||||||
interface Init $Proxy.wrap("interfaces::Init") {
|
interface Init $Proxy.wrap("interfaces::Init") {
|
||||||
construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap);
|
construct @0 (threadMap: Proxy.ThreadMap) -> (threadMap :Proxy.ThreadMap);
|
||||||
makeEcho @1 (context :Proxy.Context) -> (result :Echo.Echo);
|
makeEcho @1 (context :Proxy.Context) -> (result :Echo.Echo);
|
||||||
makeMining @2 (context :Proxy.Context) -> (result :Mining.Mining);
|
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);
|
fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address);
|
||||||
} catch (const std::system_error& e) {
|
} 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 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;
|
return nullptr;
|
||||||
}
|
}
|
||||||
throw;
|
throw;
|
||||||
|
} catch (const std::invalid_argument&) {
|
||||||
|
// Catch 'Unix address path "..." exceeded maximum socket path length' error
|
||||||
|
return nullptr;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address);
|
fd = m_process->connect(gArgs.GetDataDirNet(), "bitcoin-node", address);
|
||||||
|
|
|
@ -12,12 +12,14 @@
|
||||||
#include <consensus/validation.h>
|
#include <consensus/validation.h>
|
||||||
#include <deploymentstatus.h>
|
#include <deploymentstatus.h>
|
||||||
#include <external_signer.h>
|
#include <external_signer.h>
|
||||||
|
#include <httprpc.h>
|
||||||
#include <index/blockfilterindex.h>
|
#include <index/blockfilterindex.h>
|
||||||
#include <init.h>
|
#include <init.h>
|
||||||
#include <interfaces/chain.h>
|
#include <interfaces/chain.h>
|
||||||
#include <interfaces/handler.h>
|
#include <interfaces/handler.h>
|
||||||
#include <interfaces/mining.h>
|
#include <interfaces/mining.h>
|
||||||
#include <interfaces/node.h>
|
#include <interfaces/node.h>
|
||||||
|
#include <interfaces/rpc.h>
|
||||||
#include <interfaces/types.h>
|
#include <interfaces/types.h>
|
||||||
#include <interfaces/wallet.h>
|
#include <interfaces/wallet.h>
|
||||||
#include <kernel/chain.h>
|
#include <kernel/chain.h>
|
||||||
|
@ -80,6 +82,7 @@ using interfaces::Handler;
|
||||||
using interfaces::MakeSignalHandler;
|
using interfaces::MakeSignalHandler;
|
||||||
using interfaces::Mining;
|
using interfaces::Mining;
|
||||||
using interfaces::Node;
|
using interfaces::Node;
|
||||||
|
using interfaces::Rpc;
|
||||||
using interfaces::WalletLoader;
|
using interfaces::WalletLoader;
|
||||||
using node::BlockAssembler;
|
using node::BlockAssembler;
|
||||||
using node::BlockWaitOptions;
|
using node::BlockWaitOptions;
|
||||||
|
@ -1115,6 +1118,24 @@ public:
|
||||||
KernelNotifications& notifications() { return *Assert(m_node.notifications); }
|
KernelNotifications& notifications() { return *Assert(m_node.notifications); }
|
||||||
NodeContext& m_node;
|
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
|
||||||
} // namespace node
|
} // 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<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<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<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
|
} // namespace interfaces
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
#define BITCOIN_RPC_PROTOCOL_H
|
#define BITCOIN_RPC_PROTOCOL_H
|
||||||
|
|
||||||
//! HTTP status codes
|
//! HTTP status codes
|
||||||
enum HTTPStatusCode
|
enum HTTPStatusCode : int
|
||||||
{
|
{
|
||||||
HTTP_OK = 200,
|
HTTP_OK = 200,
|
||||||
HTTP_NO_CONTENT = 204,
|
HTTP_NO_CONTENT = 204,
|
||||||
|
|
|
@ -26,6 +26,7 @@ function(create_test_config)
|
||||||
set_configure_variable(WITH_ZMQ ENABLE_ZMQ)
|
set_configure_variable(WITH_ZMQ ENABLE_ZMQ)
|
||||||
set_configure_variable(ENABLE_EXTERNAL_SIGNER ENABLE_EXTERNAL_SIGNER)
|
set_configure_variable(ENABLE_EXTERNAL_SIGNER ENABLE_EXTERNAL_SIGNER)
|
||||||
set_configure_variable(WITH_USDT ENABLE_USDT_TRACEPOINTS)
|
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)
|
configure_file(config.ini.in config.ini USE_SOURCE_PERMISSIONS @ONLY)
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
|
@ -26,3 +26,4 @@ RPCAUTH=@abs_top_srcdir@/share/rpcauth/rpcauth.py
|
||||||
@ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true
|
@ENABLE_ZMQ_TRUE@ENABLE_ZMQ=true
|
||||||
@ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true
|
@ENABLE_EXTERNAL_SIGNER_TRUE@ENABLE_EXTERNAL_SIGNER=true
|
||||||
@ENABLE_USDT_TRACEPOINTS_TRUE@ENABLE_USDT_TRACEPOINTS=true
|
@ENABLE_USDT_TRACEPOINTS_TRUE@ENABLE_USDT_TRACEPOINTS=true
|
||||||
|
@ENABLE_IPC_TRUE@ENABLE_IPC=true
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from test_framework.blocktools import COINBASE_MATURITY
|
from test_framework.blocktools import COINBASE_MATURITY
|
||||||
from test_framework.netutil import test_ipv6_local
|
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")
|
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)
|
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__':
|
if __name__ == '__main__':
|
||||||
TestBitcoinCli(__file__).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-chainstate": ("bitcoinchainstate", "BITCOINCHAINSTATE"),
|
||||||
"bitcoin-wallet": ("bitcoinwallet", "BITCOINWALLET"),
|
"bitcoin-wallet": ("bitcoinwallet", "BITCOINWALLET"),
|
||||||
}
|
}
|
||||||
for binary, [attribute_name, env_variable_name] in binaries.items():
|
def binary_path(binary):
|
||||||
default_filename = os.path.join(
|
return os.path.join(
|
||||||
self.config["environment"]["BUILDDIR"],
|
self.config["environment"]["BUILDDIR"],
|
||||||
"bin",
|
"bin",
|
||||||
binary + self.config["environment"]["EXEEXT"],
|
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
|
return paths
|
||||||
|
|
||||||
def get_binaries(self, bin_dir=None):
|
def get_binaries(self, bin_dir=None):
|
||||||
|
@ -1007,6 +1009,11 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
||||||
if not self.is_cli_compiled():
|
if not self.is_cli_compiled():
|
||||||
raise SkipTest("bitcoin-cli has not been 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):
|
def skip_if_no_previous_releases(self):
|
||||||
"""Skip the running test if previous releases are not available."""
|
"""Skip the running test if previous releases are not available."""
|
||||||
if not self.has_previous_releases():
|
if not self.has_previous_releases():
|
||||||
|
@ -1061,5 +1068,9 @@ class BitcoinTestFramework(metaclass=BitcoinTestMetaClass):
|
||||||
"""Checks whether the wallet module was compiled with BDB support."""
|
"""Checks whether the wallet module was compiled with BDB support."""
|
||||||
return self.config["components"].getboolean("USE_BDB")
|
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):
|
def has_blockfile(self, node, filenum: str):
|
||||||
return (node.blocks_path/ f"blk{filenum}.dat").is_file()
|
return (node.blocks_path/ f"blk{filenum}.dat").is_file()
|
||||||
|
|
|
@ -355,6 +355,7 @@ BASE_SCRIPTS = [
|
||||||
'rpc_help.py',
|
'rpc_help.py',
|
||||||
'p2p_handshake.py',
|
'p2p_handshake.py',
|
||||||
'p2p_handshake.py --v2transport',
|
'p2p_handshake.py --v2transport',
|
||||||
|
'interface_ipc_cli.py',
|
||||||
'feature_dirsymlinks.py',
|
'feature_dirsymlinks.py',
|
||||||
'feature_help.py',
|
'feature_help.py',
|
||||||
'feature_shutdown.py',
|
'feature_shutdown.py',
|
||||||
|
|
Loading…
Add table
Reference in a new issue