diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ad18115bbc5..70dd1bab52f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -56,6 +56,8 @@ add_subdirectory(crypto) add_subdirectory(util) if(ENABLE_IPC) add_subdirectory(ipc) +else() + add_library(bitcoin_ipc STATIC EXCLUDE_FROM_ALL ipc/stub.cpp) endif() add_library(bitcoin_consensus STATIC EXCLUDE_FROM_ALL @@ -347,13 +349,14 @@ target_link_libraries(bitcoin_cli # Bitcoin Core RPC client if(BUILD_CLI) - add_executable(bitcoin-cli bitcoin-cli.cpp) + add_executable(bitcoin-cli bitcoin-cli.cpp init/basic.cpp) add_windows_resources(bitcoin-cli bitcoin-cli-res.rc) add_windows_application_manifest(bitcoin-cli) target_link_libraries(bitcoin-cli core_interface bitcoin_cli bitcoin_common + bitcoin_ipc bitcoin_util libevent::core libevent::extra diff --git a/src/bitcoin-cli.cpp b/src/bitcoin-cli.cpp index 2c02d87c655..24cb0344c58 100644 --- a/src/bitcoin-cli.cpp +++ b/src/bitcoin-cli.cpp @@ -11,6 +11,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -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=
", "Connect to bitcoin-node through IPC socket instead of TCP socket to execute requests. Valid
values are 'auto' to try to connect to default socket path at /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:' 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 RpcWalletName(const ArgsManager& args) @@ -791,7 +795,40 @@ struct DefaultRequestHandler : BaseRequestHandler { } }; -static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector& args, const std::optional& rpcwallet = {}) +static std::optional CallIPC(BaseRequestHandler* rh, const std::string& strMethod, const std::vector& 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 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 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 CConnectionFailed{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 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& args, const std::string& endpoint, const std::string& username) { std::string host; // In preference order, we choose the following for the port: @@ -872,7 +909,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()); @@ -888,17 +925,6 @@ 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.release(), EVHTTP_REQ_POST, endpoint.c_str()); if (r != 0) { throw CConnectionFailed("send http request failed"); @@ -958,9 +984,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", "")}; do { try { - response = CallRPC(rh, strMethod, args, rpcwallet); + if (auto ipc_response{CallIPC(rh, strMethod, args, endpoint, username)}) { + response = std::move(*ipc_response); + } else { + response = CallRPC(rh, strMethod, args, endpoint, username); + } if (fWait) { const UniValue& error = response.find_value("error"); if (!error.isNull() && error["code"].getInt() == RPC_IN_WARMUP) { diff --git a/src/httprpc.cpp b/src/httprpc.cpp index 56a243085c2..2a6751df332 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -38,13 +38,15 @@ static std::vector> g_rpcauth; static std::map> 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(); if (code == RPC_INVALID_REQUEST) @@ -52,10 +54,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 @@ -101,60 +100,23 @@ static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUserna return CheckUserAuthorized(user, pass); } -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 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)) { - LogWarning("ThreadRPCServer incorrect password attempt from %s", 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.contains(jreq.authUser); if (!user_has_whitelist && g_rpc_whitelist_default) { LogWarning("RPC User %s not allowed to call any methods", 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].contains(jreq.strMethod)) { LogWarning("RPC User %s not allowed to call method %s", 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 @@ -162,14 +124,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 @@ -183,15 +144,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].contains(strMethod)) { LogWarning("RPC User %s not allowed to call method %s", 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. @@ -218,23 +179,70 @@ 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 false; + return JSONErrorReply(JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq, status); + } +} + +static void 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; + } + // Check authorization + std::pair authHeader = req->GetHeader("authorization"); + if (!authHeader.first) { + req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA); + req->WriteReply(HTTP_UNAUTHORIZED); + return; + } + + JSONRPCRequest jreq; + jreq.context = context; + jreq.peerAddr = req->GetPeer().ToStringAddrPort(); + jreq.URI = req->GetURI(); + if (!RPCAuthorized(authHeader.second, jreq.authUser)) { + LogWarning("ThreadRPCServer incorrect password attempt from %s", 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; + } + + // 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 true; } static bool InitRPCAuthentication() diff --git a/src/httprpc.h b/src/httprpc.h index 1c1a624168f..511c82a4459 100644 --- a/src/httprpc.h +++ b/src/httprpc.h @@ -7,6 +7,10 @@ #include +class JSONRPCRequest; +class UniValue; +enum HTTPStatusCode : int; + /** Start HTTP RPC subsystem. * Precondition; HTTP and RPC has been started. */ @@ -19,6 +23,11 @@ void InterruptHTTPRPC(); */ void StopHTTPRPC(); +/** Execute a single HTTP request containing one or more JSONRPC requests. + * Specified `jreq` will be modified and `status` will be returned. + */ +UniValue ExecuteHTTPRPC(const UniValue& valRequest, JSONRPCRequest& jreq, HTTPStatusCode& status); + /** Start HTTP REST subsystem. * Precondition; HTTP and RPC has been started. */ diff --git a/src/httpserver.h b/src/httpserver.h index 5461480d44f..76381a382b3 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -50,7 +50,7 @@ void StopHTTPServer(); void UpdateHTTPServerLogging(bool enable); /** Handler for requests to a certain HTTP path */ -typedef std::function HTTPRequestHandler; +typedef std::function HTTPRequestHandler; /** Register handler for prefix. * If multiple handlers match a prefix, the first-registered one will * be invoked. diff --git a/src/init.cpp b/src/init.cpp index fb4b8fa9e27..4a45062ae49 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -719,7 +719,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) argsman.AddArg("-rpcworkqueue=", strprintf("Set the maximum depth of the work queue to service RPC calls (default: %d)", DEFAULT_HTTP_WORKQUEUE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC); argsman.AddArg("-server", "Accept command line and JSON-RPC commands", ArgsManager::ALLOW_ANY, OptionsCategory::RPC); if (can_listen_ipc) { - argsman.AddArg("-ipcbind=
", "Bind to Unix socket address and listen for incoming connections. Valid address values are \"unix\" to listen on the default path, /node.sock, or \"unix:/custom/path\" to specify a custom path. Can be specified multiple times to listen on multiple paths. Default behavior is not to listen on any path. If relative paths are specified, they are interpreted relative to the network data directory. If paths include any parent directory components and the parent directories do not exist, they will be created.", ArgsManager::ALLOW_ANY, OptionsCategory::IPC); + argsman.AddArg("-ipcbind=
", "Bind to Unix socket address and listen for incoming connections. Valid address values are \"unix\" to listen on the default path, /node.sock, or \"unix:/custom/path\" to specify a custom path. Can be specified multiple times to listen on multiple paths. Default behavior is not to listen on any path. If relative paths are specified, they are interpreted relative to the network data directory. If paths include any parent directory components and the parent directories do not exist, they will be created. Enabling this gives local processes that can access the socket unauthenticated RPC access, so it's important to choose a path with secure permissions if customizing this.", ArgsManager::ALLOW_ANY, OptionsCategory::IPC); } #if HAVE_DECL_FORK diff --git a/src/init/basic.cpp b/src/init/basic.cpp new file mode 100644 index 00000000000..12d468c0af9 --- /dev/null +++ b/src/init/basic.cpp @@ -0,0 +1,26 @@ +// 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 +#include + +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(); } +private: + std::unique_ptr m_ipc; +}; +} // namespace +} // namespace init + +namespace interfaces { +std::unique_ptr MakeBasicInit(const char* exe_name, const char* process_argv0) +{ + return std::make_unique(exe_name, process_argv0); +} +} // namespace interfaces diff --git a/src/init/bitcoin-gui.cpp b/src/init/bitcoin-gui.cpp index ca3077b9bb2..45f623d026a 100644 --- a/src/init/bitcoin-gui.cpp +++ b/src/init/bitcoin-gui.cpp @@ -7,7 +7,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -28,11 +30,13 @@ public: } std::unique_ptr makeNode() override { return interfaces::MakeNode(m_node); } std::unique_ptr makeChain() override { return interfaces::MakeChain(m_node); } + std::unique_ptr makeMining() override { return interfaces::MakeMining(m_node); } std::unique_ptr makeWalletLoader(interfaces::Chain& chain) override { return MakeWalletLoader(chain, *Assert(m_node.args)); } std::unique_ptr makeEcho() override { return interfaces::MakeEcho(); } + std::unique_ptr 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 diff --git a/src/init/bitcoin-node.cpp b/src/init/bitcoin-node.cpp index e42521938a6..b4e5b8eeb6d 100644 --- a/src/init/bitcoin-node.cpp +++ b/src/init/bitcoin-node.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ public: return MakeWalletLoader(chain, *Assert(m_node.args)); } std::unique_ptr makeEcho() override { return interfaces::MakeEcho(); } + std::unique_ptr makeRpc() override { return interfaces::MakeRpc(m_node); } interfaces::Ipc* ipc() override { return m_ipc.get(); } bool canListenIpc() override { return true; } const char* exeName() override { return EXE_NAME; } diff --git a/src/interfaces/README.md b/src/interfaces/README.md index 97167d5298f..cbb2c40f319 100644 --- a/src/interfaces/README.md +++ b/src/interfaces/README.md @@ -16,4 +16,6 @@ The following interfaces are defined here: * [`Ipc`](ipc.h) — used by multiprocess code to access `Init` interface across processes. Added in [#19160](https://github.com/bitcoin/bitcoin/pull/19160). +* [`Rpc`](rpc.h) — used by `bitcoin-cli` to be able to call RPC methods over a unix socket instead of TCP. + The interfaces above define boundaries between major components of bitcoin code (node, wallet, and gui), making it possible for them to run in [different processes](../../doc/multiprocess.md), and be tested, developed, and understood independently. These interfaces are not currently designed to be stable or to be used externally. diff --git a/src/interfaces/init.h b/src/interfaces/init.h index 463d43e7c2e..d5b394b0e23 100644 --- a/src/interfaces/init.h +++ b/src/interfaces/init.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,7 @@ public: virtual std::unique_ptr makeMining() { return nullptr; } virtual std::unique_ptr makeWalletLoader(Chain& chain) { return nullptr; } virtual std::unique_ptr makeEcho() { return nullptr; } + virtual std::unique_ptr makeRpc() { return nullptr; } virtual Ipc* ipc() { return nullptr; } virtual bool canListenIpc() { return false; } virtual const char* exeName() { return nullptr; } @@ -55,6 +57,25 @@ std::unique_ptr MakeWalletInit(int argc, char* argv[], int& exit_status); //! Return implementation of Init interface for the gui process. std::unique_ptr 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 MakeBasicInit(const char* exe_name, const char* process_argv0=""); } // namespace interfaces #endif // BITCOIN_INTERFACES_INIT_H diff --git a/src/interfaces/ipc.h b/src/interfaces/ipc.h index f94ffa3c9d7..23b46fb6826 100644 --- a/src/interfaces/ipc.h +++ b/src/interfaces/ipc.h @@ -7,6 +7,7 @@ #include #include +#include #include namespace ipc { diff --git a/src/interfaces/rpc.h b/src/interfaces/rpc.h new file mode 100644 index 00000000000..d8d5566ad87 --- /dev/null +++ b/src/interfaces/rpc.h @@ -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 +#include + +class UniValue; + +namespace node { +struct NodeContext; +} // namespace node + +namespace interfaces { +//! Interface giving clients ability to emulate HTTP 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 MakeRpc(node::NodeContext& node); + +} // namespace interfaces + +#endif // BITCOIN_INTERFACES_RPC_H diff --git a/src/ipc/CMakeLists.txt b/src/ipc/CMakeLists.txt index 5378ef1924b..8326423d8bc 100644 --- a/src/ipc/CMakeLists.txt +++ b/src/ipc/CMakeLists.txt @@ -14,6 +14,7 @@ target_capnp_sources(bitcoin_ipc ${CMAKE_CURRENT_SOURCE_DIR} capnp/echo.capnp capnp/init.capnp capnp/mining.capnp + capnp/rpc.capnp ) target_link_libraries(bitcoin_ipc diff --git a/src/ipc/capnp/init-types.h b/src/ipc/capnp/init-types.h index 2abd7b211e1..c6764d2a280 100644 --- a/src/ipc/capnp/init-types.h +++ b/src/ipc/capnp/init-types.h @@ -7,5 +7,6 @@ #include #include +#include #endif // BITCOIN_IPC_CAPNP_INIT_TYPES_H diff --git a/src/ipc/capnp/init.capnp b/src/ipc/capnp/init.capnp index a20ef2fcaf3..d95bfb7426a 100644 --- a/src/ipc/capnp/init.capnp +++ b/src/ipc/capnp/init.capnp @@ -15,11 +15,13 @@ $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 @3 (context :Proxy.Context) -> (result :Mining.Mining); + makeRpc @4 (context :Proxy.Context) -> (result :Rpc.Rpc); # DEPRECATED: no longer supported; server returns an error. makeMiningOld2 @2 () -> (); diff --git a/src/ipc/capnp/rpc-types.h b/src/ipc/capnp/rpc-types.h new file mode 100644 index 00000000000..4d385dee312 --- /dev/null +++ b/src/ipc/capnp/rpc-types.h @@ -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 +#include +#include + +#endif // BITCOIN_IPC_CAPNP_RPC_TYPES_H diff --git a/src/ipc/capnp/rpc.capnp b/src/ipc/capnp/rpc.capnp new file mode 100644 index 00000000000..c831424a046 --- /dev/null +++ b/src/ipc/capnp/rpc.capnp @@ -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); +} diff --git a/src/ipc/interfaces.cpp b/src/ipc/interfaces.cpp index f40913d1853..1171f706487 100644 --- a/src/ipc/interfaces.cpp +++ b/src/ipc/interfaces.cpp @@ -95,10 +95,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); diff --git a/src/ipc/stub.cpp b/src/ipc/stub.cpp new file mode 100644 index 00000000000..e00ff568f82 --- /dev/null +++ b/src/ipc/stub.cpp @@ -0,0 +1,14 @@ +// 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 + +#include + +namespace interfaces { +std::unique_ptr MakeIpc(const char* exe_name, const char* process_argv0, Init& init) +{ + return {}; +} +} // namespace interfaces diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index a8d91bf6255..15f14007fa5 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -12,12 +12,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -81,6 +83,7 @@ using interfaces::Handler; using interfaces::MakeSignalHandler; using interfaces::Mining; using interfaces::Node; +using interfaces::Rpc; using interfaces::WalletLoader; using kernel::ChainstateRole; using node::BlockAssembler; @@ -1015,6 +1018,24 @@ public: bool m_interrupt_mining{false}; 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 @@ -1034,4 +1055,5 @@ std::unique_ptr MakeMining(node::NodeContext& context, bool wait_loaded) } return std::make_unique(context); } +std::unique_ptr MakeRpc(node::NodeContext& context) { return std::make_unique(context); } } // namespace interfaces diff --git a/src/rpc/protocol.h b/src/rpc/protocol.h index 3f18365c50b..40e685d5868 100644 --- a/src/rpc/protocol.h +++ b/src/rpc/protocol.h @@ -7,7 +7,7 @@ #define BITCOIN_RPC_PROTOCOL_H //! HTTP status codes -enum HTTPStatusCode +enum HTTPStatusCode : int { HTTP_OK = 200, HTTP_NO_CONTENT = 204, diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index f626ee51761..fbe753a65fa 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -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 @@ -434,6 +435,16 @@ 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_compiled(): + # This tests behavior when ENABLE_IPC is off. When it is on, + # behavior is checked by the interface_ipc_cli.py test. + 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() diff --git a/test/functional/interface_ipc_cli.py b/test/functional/interface_ipc_cli.py new file mode 100755 index 00000000000..92ccc879fba --- /dev/null +++ b/test/functional/interface_ipc_cli.py @@ -0,0 +1,68 @@ +#!/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 + +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): + self.extra_init = [{"ipcbind": True}] + 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 is None: + assert_equal(result.stdout, '[\n "foo"\n]\n') + else: + assert_equal(result.stdout, error) + assert_equal(result.stderr, None) + assert_equal(result.returncode, 0 if error is None else 1) + + def run_test(self): + node = self.nodes[0] + if node.ipc_tmp_dir: + 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(node.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" + ipc_connect_error = "error: timeout on transient 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" + ipc_http_conflict = "error: -rpcconnect and -ipcconnect options cannot both be enabled\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 not node.ipc_tmp_dir: + 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:{node.ipc_socket_path}"], ipc_error) + self.test_cli(["-noipcconnect"], http_error) + self.test_cli(["-ipcconnect=unix", "-rpcconnect=127.0.0.1"], ipc_http_conflict) + + self.stop_node(0) + + +if __name__ == '__main__': + TestBitcoinIpcCli(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 005df69b057..9b71eba8212 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -379,6 +379,7 @@ BASE_SCRIPTS = [ 'tool_rpcauth.py', 'p2p_handshake.py', 'p2p_handshake.py --v2transport', + 'interface_ipc_cli.py', 'feature_dirsymlinks.py', 'feature_help.py', 'feature_framework_startup_failures.py',