Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
71 changes: 57 additions & 14 deletions src/bitcoin-cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -792,7 +796,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 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<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:
Expand Down Expand Up @@ -873,7 +910,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());
Expand All @@ -889,17 +926,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");
Expand Down Expand Up @@ -959,9 +985,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", "")};
Comment thread
ryanofsky marked this conversation as resolved.
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<int>() == RPC_IN_WARMUP) {
Expand Down
144 changes: 76 additions & 68 deletions src/httprpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,23 @@ 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)
nStatus = HTTP_BAD_REQUEST;
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
Expand Down Expand Up @@ -101,75 +100,37 @@ 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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b56a36a

Looking at this with fresh eyes after some time, and I'm wondering why to expose this to the interface instead of going directly to JSONRPCExec()? Or essentially pull out the chunk of this function starting from // Execute each request .

We shouldn't need HTTP status codes at all for RPC over IPC

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #32297 (comment)

Reason for calling the HTTP function instead of the JSONRPCExec is just that the HTTP function provides some additional functionality like user authorization and method whitelisting. I imagine in the future it could also provide more functionality like having different default wallets or different settings for different users.

It would be reasonable for the IPC inteface to just expose the JSON-RPC methods without any higher level functionality, but since the IPC interface is used by bitcoin-cli I think it makes sense to provide whatever allows bitcoin-cli to be fully functional.

{
// JSONRPC handles only POST
if (req->GetRequestMethod() != HTTPRequest::POST) {
req->WriteReply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests");
return false;
}
// Check authorization
Comment thread
ryanofsky marked this conversation as resolved.
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)) {
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
// exceptions which return HTTP errors and RPC errors to the client.
// 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
Expand All @@ -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.
Expand All @@ -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<bool, std::string> 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()
Expand Down
9 changes: 9 additions & 0 deletions src/httprpc.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

#include <any>

class JSONRPCRequest;
class UniValue;
enum HTTPStatusCode : int;

/** Start HTTP RPC subsystem.
* Precondition; HTTP and RPC has been started.
*/
Expand All @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/httpserver.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ void StopHTTPServer();
void UpdateHTTPServerLogging(bool enable);

/** Handler for requests to a certain HTTP path */
typedef std::function<bool(HTTPRequest* req, const std::string &)> HTTPRequestHandler;
typedef std::function<void(HTTPRequest* req, const std::string &)> HTTPRequestHandler;
/** Register handler for prefix.
* If multiple handlers match a prefix, the first-registered one will
* be invoked.
Expand Down
2 changes: 1 addition & 1 deletion src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,7 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc)
argsman.AddArg("-rpcworkqueue=<n>", 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=<address>", "Bind to Unix socket address and listen for incoming connections. Valid address values are \"unix\" to listen on the default path, <datadir>/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=<address>", "Bind to Unix socket address and listen for incoming connections. Valid address values are \"unix\" to listen on the default path, <datadir>/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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be doing something wrong here, but I am not seeing this help message in bitcoind -h or bitcoin node -h. Stepping through with debugger, the reason seems to be that SetupServerArgs() is called on the NodeContext's interfaces::Init which has canListenIpc() { return false; }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #32297 (comment)

Nice find. The current behavior is expected but pointlessly confusing.

The goal of this code is to show -ipcbind help when bitcoin-node -h is run and not show it when bitcoind -h is run, which is what you are observing.

The problem is the bitcoin wrapper prefers to call bitcoind instead of bitcoin-node when no IPC options are specified. So when you run bitcoin node -h you won't see any IPC help. A fix for this would be to make the wrapper prefer bitcoin-node when help is requested like:

--- a/src/bitcoin.cpp
+++ b/src/bitcoin.cpp
@@ -168,7 +168,7 @@ bool UseMultiprocess(const CommandLine& cmd)
 
     // If any -ipc* options are set these need to be processed by a
     // multiprocess-capable binary.
-    return args.IsArgSet("-ipcbind") || args.IsArgSet("-ipcconnect") || args.IsArgSet("-ipcfd");
+    return args.IsArgSet("-ipcbind") || args.IsArgSet("-ipcconnect") || args.IsArgSet("-ipcfd") || HelpRequested(args);
 }
 
 //! Execute the specified bitcoind, bitcoin-qt or other command line in `args`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh gotcha, bitcoin-node vs bitcoin node explains what I had here and not a blocker for this PR

}

#if HAVE_DECL_FORK
Expand Down
Loading
Loading