diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 9881b3c8b215d..9fbd0be71b7c8 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -154,6 +154,8 @@ static struct evhttp* eventHTTP = nullptr; static std::vector rpc_allow_subnets; //! Work queue for handling longer requests off the event loop thread static std::unique_ptr> g_work_queue{nullptr}; +//! List of 'external' RPC users +static std::vector g_external_usernames; //! Handlers for (sub)paths static std::vector pathHandlers; //! Bound listening sockets @@ -267,8 +269,7 @@ static void http_request_cb(struct evhttp_request* req, void* arg) } } const bool is_external_request = [&hreq]() -> bool { - const std::string external_username = gArgs.GetArg("-rpcexternaluser", ""); - if (external_username.empty()) return false; + if (g_external_usernames.empty()) return false; const std::string strAuth = hreq->GetHeader("authorization").second; if (strAuth.substr(0, 6) != "Basic ") @@ -280,8 +281,8 @@ static void http_request_cb(struct evhttp_request* req, void* arg) if (invalid) return false; if (strUserPass.find(':') == std::string::npos) return false; - - return strUserPass.substr(0, strUserPass.find(':')) == external_username; + const std::string username{strUserPass.substr(0, strUserPass.find(':'))}; + return find(g_external_usernames.begin(), g_external_usernames.end(), username) != g_external_usernames.end(); }(); // Dispatch to worker thread @@ -429,12 +430,11 @@ bool InitHTTPServer() LogPrint(BCLog::HTTP, "Initialized HTTP server\n"); int workQueueDepth = std::max((long)gArgs.GetArg("-rpcworkqueue", DEFAULT_HTTP_WORKQUEUE), 1L); int workQueueDepthExternal = 0; - if (!gArgs.GetArg("-rpcexternaluser", "").empty()) { - LogPrintf("HTTP: creating external work queue of depth %d\n", workQueueDepthExternal); + if (const std::string rpc_externaluser{gArgs.GetArg("-rpcexternaluser", "")}; !rpc_externaluser.empty()) { workQueueDepthExternal = std::max((long)gArgs.GetArg("-rpcexternalworkqueue", DEFAULT_HTTP_WORKQUEUE), 1L); + g_external_usernames = SplitString(rpc_externaluser, ','); } LogPrintf("HTTP: creating work queue of depth %d external_depth %d\n", workQueueDepth, workQueueDepthExternal); - g_work_queue = std::make_unique>(workQueueDepth, workQueueDepthExternal); // transfer ownership to eventBase/HTTP via .release() eventBase = base_ctr.release(); diff --git a/src/init.cpp b/src/init.cpp index 8a0701960d6c2..674468b798ab9 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -767,7 +767,7 @@ void SetupServerArgs(NodeContext& node) argsman.AddArg("-rpcauth=", "Username and HMAC-SHA-256 hashed password for JSON-RPC connections. The field comes in the format: :$. A canonical python script is included in share/rpcuser. The client then connects normally using the rpcuser=/rpcpassword= pair of arguments. This option can be specified multiple times", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC); argsman.AddArg("-rpcbind=[:port]", "Bind to given address to listen for JSON-RPC connections. Do not expose the RPC server to untrusted networks such as the public internet! This option is ignored unless -rpcallowip is also passed. Port is optional and overrides -rpcport. Use [host]:port notation for IPv6. This option can be specified multiple times (default: 127.0.0.1 and ::1 i.e., localhost, or if -rpcallowip has been specified, 0.0.0.0 and :: i.e., all addresses)", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY | ArgsManager::SENSITIVE, OptionsCategory::RPC); argsman.AddArg("-rpccookiefile=", "Location of the auth cookie. Relative paths will be prefixed by a net-specific datadir location. (default: data dir)", ArgsManager::ALLOW_ANY, OptionsCategory::RPC); - argsman.AddArg("-rpcexternaluser=", "Username for JSON-RPC external connections", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC); + argsman.AddArg("-rpcexternaluser=", "List of comma-separated usernames for JSON-RPC external connections", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC); argsman.AddArg("-rpcexternalworkqueue=", strprintf("Set the depth of the work queue to service external RPC calls (default: %d)", DEFAULT_HTTP_WORKQUEUE), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::RPC); argsman.AddArg("-rpcpassword=", "Password for JSON-RPC connections", ArgsManager::ALLOW_ANY | ArgsManager::SENSITIVE, OptionsCategory::RPC); argsman.AddArg("-rpcport=", strprintf("Listen for JSON-RPC connections on (default: %u, testnet: %u, regtest: %u)", defaultBaseParams->RPCPort(), testnetBaseParams->RPCPort(), regtestBaseParams->RPCPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::RPC); diff --git a/test/functional/rpc_platform_filter.py b/test/functional/rpc_platform_filter.py index a0ec25e6a555b..200ed96f48d15 100755 --- a/test/functional/rpc_platform_filter.py +++ b/test/functional/rpc_platform_filter.py @@ -114,7 +114,7 @@ def test_command(method, params, auth, expected_status, should_not_match=False): test_command("debug", ["1"], rpcuser_authpair_operator, 200) - self.log.info("Restart node with -rpcexternaluser...") + self.log.info("Restart node with -rpcexternaluser") self.restart_node(0, extra_args=["-rpcexternaluser=platform-user"]) external_log_str = "HTTP: Calling handler for external user" @@ -124,6 +124,13 @@ def test_command(method, params, auth, expected_status, should_not_match=False): with self.nodes[0].assert_debug_log(expected_msgs=[expected_log_str], unexpected_msgs = [external_log_str]): test_command("getbestblockhash", [], rpcuser_authpair_operator, 200) + self.log.info("Restart node with multiple external users") + self.restart_node(0, extra_args=["-rpcexternaluser=platform-user,operator"]) + with self.nodes[0].assert_debug_log(expected_msgs=[expected_log_str, external_log_str]): + test_command("getbestblockhash", [], rpcuser_authpair_platform, 200) + with self.nodes[0].assert_debug_log(expected_msgs=[expected_log_str, external_log_str]): + test_command("getbestblockhash", [], rpcuser_authpair_operator, 200) + if __name__ == '__main__':