dash/src/httprpc.cpp
pasta 7330982631
Merge #6100: feat: make whitelist works with composite commands for platform needs
85abbb97b4 chore: add release notes for composite command for whitelist (Konstantin Akimov)
78ad778bb0 feat: test composite commands in functional test for whitelist (Konstantin Akimov)
a102a59787 feat: add support of composite commands in RPC'c whitelists (Konstantin Akimov)

Pull request description:

  ## Issue being fixed or feature implemented
  https://github.com/dashpay/dash-issues/issues/66
  https://github.com/dashpay/dash-issues/issues/65

  ## What was done?
  Our composite commands such as "quorum list" have been refactored to make them truly compatible with other features, such as whitelist, see https://github.com/dashpay/dash/pull/6052 https://github.com/dashpay/dash/pull/6051 https://github.com/dashpay/dash/pull/6055 and other related PRs

  This PR makes whitelist feature to be compatible with composite commands.

  Instead implementing additional users such "dapi" better to provide universal way which do not require new build for every new API that has been used by platform, let's simplify things.

  Platform at their side can use config such as this one (created based on shumkov's example):
  ```
  rpc: {
            host: '127.0.0.1',
            port: 9998,
            users: [
              {
                user: 'dashmate',
                password: 'rpcpassword',
                whitelist: null,
                lowPriority: false,
              },
              {
                username: 'platform-dapi',
                password: 'rpcpassword',
                whitelist: [],
                lowPriority: true,
              },
              {
                username: 'platform-drive-consensus',
                password: 'rpcpassword',
                whitelist: [getbestchainlock,getblockchaininfo,getrawtransaction,submitchainlock,verifychainlock,protx_listdiff,quorum_listextended,quorum_info,getassetunlockstatuses,sendrawtransaction,mnsync_status]
                lowPriority: false,
              },
              {
                username: 'platform-drive-other',
                password: 'rpcpassword',
                whitelist: [getbestchainlock,getblockchaininfo,getrawtransaction,submitchainlock,verifychainlock,protx_listdiff,quorum_listextended,quorum_info,getassetunlockstatuses,sendrawtransaction,mnsync_status]
  ],
                lowPriority: true,
              },
            ],
            allowIps: ['127.0.0.1', '172.16.0.0/12', '192.168.0.0/16'],
          },
  ```

  ## How Has This Been Tested?
  Updated functional tests, see commits

  ## Breaking Changes
  n/a

  ## Checklist:
  - [x] I have performed a self-review of my own code
  - [x] I have commented my code, particularly in hard-to-understand areas
  - [x] I have added or updated relevant unit/integration/functional/e2e tests
  - [ ] I have made corresponding changes to the documentation
  - [x] I have assigned this pull request to a milestone

ACKs for top commit:
  UdjinM6:
    LGTM, utACK 85abbb97b4
  PastaPastaPasta:
    utACK 85abbb97b4

Tree-SHA512: 88608179c347420269880c352cf9f3b46272f3fc62e8e7158042e53ad69dc460d5210a1f89e1e09081d090250c87fcececade88e2ddec09f73f1175836d7867b
2024-07-15 11:51:59 -05:00

367 lines
13 KiB
C++

// Copyright (c) 2015-2020 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 <httprpc.h>
#include <chainparams.h>
#include <crypto/hmac_sha256.h>
#include <httpserver.h>
#include <rpc/protocol.h>
#include <rpc/server.h>
#include <util/strencodings.h>
#include <util/string.h>
#include <util/system.h>
#include <util/translation.h>
#include <walletinitinterface.h>
#include <algorithm>
#include <iterator>
#include <map>
#include <memory>
#include <set>
#include <string>
/** WWW-Authenticate to present with 401 Unauthorized response */
static const char* WWW_AUTH_HEADER_DATA = "Basic realm=\"jsonrpc\"";
/** Simple one-shot callback timer to be used by the RPC mechanism to e.g.
* re-lock the wallet.
*/
class HTTPRPCTimer : public RPCTimerBase
{
public:
HTTPRPCTimer(struct event_base* eventBase, std::function<void()>& func, int64_t millis) :
ev(eventBase, false, func)
{
struct timeval tv;
tv.tv_sec = millis/1000;
tv.tv_usec = (millis%1000)*1000;
ev.trigger(&tv);
}
private:
HTTPEvent ev;
};
class HTTPRPCTimerInterface : public RPCTimerInterface
{
public:
explicit HTTPRPCTimerInterface(struct event_base* _base) : base(_base)
{
}
const char* Name() override
{
return "HTTP";
}
RPCTimerBase* NewTimer(std::function<void()>& func, int64_t millis) override
{
return new HTTPRPCTimer(base, func, millis);
}
private:
struct event_base* base;
};
/* Pre-base64-encoded authentication token */
static std::string strRPCUserColonPass;
/* Stored RPC timer interface (for unregistration) */
static std::unique_ptr<HTTPRPCTimerInterface> httpRPCTimerInterface;
/* List of -rpcauth values */
static std::vector<std::vector<std::string>> g_rpcauth;
/* RPC Auth Whitelist */
static std::map<std::string, std::set<std::string>> g_rpc_whitelist;
static bool g_rpc_whitelist_default = false;
extern std::vector<std::string> g_external_usernames;
class RpcHttpRequest
{
public:
HTTPRequest* m_req;
int64_t m_startTime;
int m_status{0};
std::string user;
std::string command;
RpcHttpRequest(HTTPRequest* req) :
m_req{req},
m_startTime{GetTimeMicros()}
{}
~RpcHttpRequest()
{
const bool is_external = find(g_external_usernames.begin(), g_external_usernames.end(), user) != g_external_usernames.end();
LogPrint(BCLog::BENCHMARK, "HTTP RPC request handled: user=%s command=%s external=%s status=%d elapsed_time_ms=%d\n", user, command, is_external, m_status, (GetTimeMicros() - m_startTime) / 1000);
}
bool send_reply(int status, const std::string& response = "")
{
m_status = status;
m_req->WriteReply(status, response);
return m_status == HTTP_OK;
}
};
static bool whitelisted(JSONRPCRequest jreq)
{
if (g_rpc_whitelist[jreq.authUser].count(jreq.strMethod)) return true;
// check for composite command after
if (!jreq.params.isArray() || jreq.params.empty()) return false;
if (!jreq.params[0].isStr()) return false;
return g_rpc_whitelist[jreq.authUser].count(jreq.strMethod + jreq.params[0].get_str());
}
static bool JSONErrorReply(RpcHttpRequest& rpcRequest, const UniValue& objError, const UniValue& id)
{
// Send error reply from json-rpc error object
int nStatus = HTTP_INTERNAL_SERVER_ERROR;
int code = find_value(objError, "code").get_int();
if (code == RPC_INVALID_REQUEST)
nStatus = HTTP_BAD_REQUEST;
else if (code == RPC_METHOD_NOT_FOUND)
nStatus = HTTP_NOT_FOUND;
else if (code == RPC_PLATFORM_RESTRICTION) {
nStatus = HTTP_FORBIDDEN;
}
std::string strReply = JSONRPCReply(NullUniValue, objError, id);
rpcRequest.m_req->WriteHeader("Content-Type", "application/json");
return rpcRequest.send_reply(nStatus, strReply);
}
//This function checks username and password against -rpcauth
//entries from config file.
static bool multiUserAuthorized(std::string strUserPass)
{
if (strUserPass.find(':') == std::string::npos) {
return false;
}
std::string strUser = strUserPass.substr(0, strUserPass.find(':'));
std::string strPass = strUserPass.substr(strUserPass.find(':') + 1);
for (const auto& vFields : g_rpcauth) {
std::string strName = vFields[0];
if (!TimingResistantEqual(strName, strUser)) {
continue;
}
std::string strSalt = vFields[1];
std::string strHash = vFields[2];
static const unsigned int KEY_SIZE = 32;
unsigned char out[KEY_SIZE];
CHMAC_SHA256(reinterpret_cast<const unsigned char*>(strSalt.data()), strSalt.size()).Write(reinterpret_cast<const unsigned char*>(strPass.data()), strPass.size()).Finalize(out);
std::vector<unsigned char> hexvec(out, out+KEY_SIZE);
std::string strHashFromPass = HexStr(hexvec);
if (TimingResistantEqual(strHashFromPass, strHash)) {
return true;
}
}
return false;
}
static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUsernameOut)
{
if (strRPCUserColonPass.empty()) // Belt-and-suspenders measure if InitRPCAuthentication was not called
return false;
if (strAuth.substr(0, 6) != "Basic ")
return false;
std::string strUserPass64 = TrimString(strAuth.substr(6));
bool invalid;
std::string strUserPass = DecodeBase64(strUserPass64, &invalid);
if (invalid) return false;
if (strUserPass.find(':') != std::string::npos)
strAuthUsernameOut = strUserPass.substr(0, strUserPass.find(':'));
//Check if authorized under single-user field
if (TimingResistantEqual(strUserPass, strRPCUserColonPass)) {
return true;
}
return multiUserAuthorized(strUserPass);
}
static bool HTTPReq_JSONRPC(const CoreContext& context, HTTPRequest* req)
{
RpcHttpRequest rpcRequest(req);
// JSONRPC handles only POST
if (req->GetRequestMethod() != HTTPRequest::POST) {
return rpcRequest.send_reply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests");
}
// Check authorization
std::pair<bool, std::string> authHeader = req->GetHeader("authorization");
if (!authHeader.first) {
req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
return rpcRequest.send_reply(HTTP_UNAUTHORIZED);
}
JSONRPCRequest jreq(context);
jreq.peerAddr = req->GetPeer().ToString();
if (!RPCAuthorized(authHeader.second, rpcRequest.user)) {
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);
return rpcRequest.send_reply(HTTP_UNAUTHORIZED);
}
jreq.authUser = rpcRequest.user;
try {
// Parse request
UniValue valRequest;
if (!valRequest.read(req->ReadBody()))
throw JSONRPCError(RPC_PARSE_ERROR, "Parse error");
// Set the URI
jreq.URI = req->GetURI();
std::string strReply;
bool user_has_whitelist = g_rpc_whitelist.count(jreq.authUser);
if (!user_has_whitelist && g_rpc_whitelist_default) {
LogPrintf("RPC User %s not allowed to call any methods\n", jreq.authUser);
return rpcRequest.send_reply(HTTP_FORBIDDEN);
// singleton request
} else if (valRequest.isObject()) {
jreq.parse(valRequest);
rpcRequest.command = jreq.strMethod;
if (user_has_whitelist && !whitelisted(jreq)) {
LogPrintf("RPC User %s not allowed to call method %s\n", jreq.authUser, jreq.strMethod);
return rpcRequest.send_reply(HTTP_FORBIDDEN);
}
UniValue result = tableRPC.execute(jreq);
// Send reply
strReply = JSONRPCReply(result, NullUniValue, jreq.id);
// array of requests
} else if (valRequest.isArray()) {
if (user_has_whitelist) {
for (unsigned int reqIdx = 0; reqIdx < valRequest.size(); reqIdx++) {
if (!valRequest[reqIdx].isObject()) {
throw JSONRPCError(RPC_INVALID_REQUEST, "Invalid Request object");
} else {
const UniValue& request = valRequest[reqIdx].get_obj();
// Parse method
std::string strMethod = find_value(request, "method").get_str();
if (!whitelisted(jreq)) {
LogPrintf("RPC User %s not allowed to call method %s\n", jreq.authUser, strMethod);
return rpcRequest.send_reply(HTTP_FORBIDDEN);
}
}
}
}
strReply = JSONRPCExecBatch(jreq, valRequest.get_array());
}
else
throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");
req->WriteHeader("Content-Type", "application/json");
return rpcRequest.send_reply(HTTP_OK, strReply);
} catch (const UniValue& objError) {
return JSONErrorReply(rpcRequest, objError, jreq.id);
} catch (const std::exception& e) {
return JSONErrorReply(rpcRequest, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
}
assert(false);
}
static bool InitRPCAuthentication()
{
if (gArgs.GetArg("-rpcpassword", "") == "")
{
LogPrintf("Using random cookie authentication.\n");
if (!GenerateAuthCookie(&strRPCUserColonPass)) {
return false;
}
} else {
LogPrintf("Config options rpcuser and rpcpassword will soon be deprecated. Locally-run instances may remove rpcuser to use cookie-based auth, or may be replaced with rpcauth. Please see share/rpcauth for rpcauth auth generation.\n");
strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" + gArgs.GetArg("-rpcpassword", "");
}
if (gArgs.GetArg("-rpcauth","") != "")
{
LogPrintf("Using rpcauth authentication.\n");
for (const std::string& rpcauth : gArgs.GetArgs("-rpcauth")) {
std::vector<std::string> fields = SplitString(rpcauth, ":$");
if (fields.size() == 3) {
g_rpcauth.push_back(fields);
} else {
LogPrintf("Invalid -rpcauth argument.\n");
return false;
}
}
}
g_rpc_whitelist_default = gArgs.GetBoolArg("-rpcwhitelistdefault", gArgs.IsArgSet("-rpcwhitelist"));
for (const std::string& strRPCWhitelist : gArgs.GetArgs("-rpcwhitelist")) {
auto pos = strRPCWhitelist.find(':');
std::string strUser = strRPCWhitelist.substr(0, pos);
bool intersect = g_rpc_whitelist.count(strUser);
std::set<std::string>& whitelist = g_rpc_whitelist[strUser];
if (pos != std::string::npos) {
std::string strWhitelist = strRPCWhitelist.substr(pos + 1);
std::vector<std::string> whitelist_split = SplitString(strWhitelist, ", ");
std::set<std::string> new_whitelist{
std::make_move_iterator(whitelist_split.begin()),
std::make_move_iterator(whitelist_split.end())};
if (intersect) {
std::set<std::string> tmp_whitelist;
std::set_intersection(new_whitelist.begin(), new_whitelist.end(),
whitelist.begin(), whitelist.end(), std::inserter(tmp_whitelist, tmp_whitelist.end()));
new_whitelist = std::move(tmp_whitelist);
}
whitelist = std::move(new_whitelist);
}
}
return true;
}
bool StartHTTPRPC(const CoreContext& context)
{
LogPrint(BCLog::RPC, "Starting HTTP RPC server\n");
if (!InitRPCAuthentication())
return false;
auto handle_rpc = [&context](HTTPRequest* req, const std::string&) { return HTTPReq_JSONRPC(context, req); };
RegisterHTTPHandler("/", true, handle_rpc);
if (g_wallet_init_interface.HasWalletSupport()) {
RegisterHTTPHandler("/wallet/", false, handle_rpc);
}
struct event_base* eventBase = EventBase();
assert(eventBase);
httpRPCTimerInterface = std::make_unique<HTTPRPCTimerInterface>(eventBase);
RPCSetTimerInterface(httpRPCTimerInterface.get());
return true;
}
void InterruptHTTPRPC()
{
LogPrint(BCLog::RPC, "Interrupting HTTP RPC server\n");
}
void StopHTTPRPC()
{
LogPrint(BCLog::RPC, "Stopping HTTP RPC server\n");
UnregisterHTTPHandler("/", true);
if (g_wallet_init_interface.HasWalletSupport()) {
UnregisterHTTPHandler("/wallet/", false);
}
if (httpRPCTimerInterface) {
RPCUnsetTimerInterface(httpRPCTimerInterface.get());
httpRPCTimerInterface.reset();
}
}