mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-29 14:59:39 -04:00
refactor: Add ExecuteHTTPRPC function
Add ExecuteHTTPRPC to provide a way to execute an HTTP request without relying on HTTPRequest and libevent types. Behavior is not changing in any way, this is just moving code. This commit may be easiest to review using git's --color-moved option.
This commit is contained in:
parent
679bb2aac2
commit
ea98a42640
3 changed files with 84 additions and 68 deletions
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.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue