cli: Clearer error messages on authentication failure

Co-authored-by: MarcoFalke <*~=`'#}+{/-|&$^_@721217.xyz>
This commit is contained in:
Hodlinator
2026-03-31 12:25:05 +02:00
parent 84c3f8d325
commit 20a94c1524
5 changed files with 41 additions and 28 deletions

View File

@@ -901,15 +901,13 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
evhttp_request_set_error_cb(req.get(), http_error_cb);
// Get credentials
std::string strRPCUserColonPass;
bool failedToGetAuthCookie = false;
std::string rpc_credentials;
std::optional<AuthCookieResult> auth_cookie_result;
if (gArgs.GetArg("-rpcpassword", "") == "") {
// Try fall back to cookie-based authentication if no password is provided
if (!GetAuthCookie(&strRPCUserColonPass)) {
failedToGetAuthCookie = true;
}
auth_cookie_result = GetAuthCookie(rpc_credentials);
} else {
strRPCUserColonPass = username + ":" + gArgs.GetArg("-rpcpassword", "");
rpc_credentials = username + ":" + gArgs.GetArg("-rpcpassword", "");
}
struct evkeyvalq* output_headers = evhttp_request_get_output_headers(req.get());
@@ -917,7 +915,7 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
evhttp_add_header(output_headers, "Host", host.c_str());
evhttp_add_header(output_headers, "Connection", "close");
evhttp_add_header(output_headers, "Content-Type", "application/json");
evhttp_add_header(output_headers, "Authorization", (std::string("Basic ") + EncodeBase64(strRPCUserColonPass)).c_str());
evhttp_add_header(output_headers, "Authorization", (std::string("Basic ") + EncodeBase64(rpc_credentials)).c_str());
// Attach request data
std::string strRequest = rh->PrepareRequest(strMethod, args).write() + "\n";
@@ -942,13 +940,24 @@ static UniValue CallRPC(BaseRequestHandler* rh, const std::string& strMethod, co
"Use \"bitcoin-cli -help\" for more info.",
host, port, responseErrorMessage));
} else if (response.status == HTTP_UNAUTHORIZED) {
if (failedToGetAuthCookie) {
throw std::runtime_error(strprintf(
"Could not locate RPC credentials. No authentication cookie could be found, and RPC password is not set. See -rpcpassword and -stdinrpcpass. Configuration file: (%s)",
fs::PathToString(gArgs.GetConfigFilePath())));
std::string error{"Authorization failed: "};
if (auth_cookie_result.has_value()) {
switch (*auth_cookie_result) {
case AuthCookieResult::Error:
error += "Failed to read cookie file and no rpcpassword was specified.";
break;
case AuthCookieResult::Disabled:
error += "Cookie file was disabled via -norpccookiefile and no rpcpassword was specified.";
break;
case AuthCookieResult::Ok:
error += "Cookie file credentials were invalid and no rpcpassword was specified.";
break;
}
} else {
throw std::runtime_error("Authorization failed: Incorrect rpcuser or rpcpassword");
error += "Incorrect rpcuser or rpcpassword were specified.";
}
error += strprintf(" Configuration file: (%s)", fs::PathToString(gArgs.GetConfigFilePath()));
throw std::runtime_error(error);
} else if (response.status == HTTP_SERVICE_UNAVAILABLE) {
throw std::runtime_error(strprintf("Server response: %s", response.body));
} else if (response.status >= 400 && response.status != HTTP_BAD_REQUEST && response.status != HTTP_NOT_FOUND && response.status != HTTP_INTERNAL_SERVER_ERROR)

View File

@@ -145,23 +145,20 @@ AuthCookieResult GenerateAuthCookie(const std::optional<fs::perms>& cookie_perms
return AuthCookieResult::Ok;
}
bool GetAuthCookie(std::string *cookie_out)
AuthCookieResult GetAuthCookie(std::string& cookie_out)
{
std::ifstream file;
std::string cookie;
fs::path filepath = GetAuthCookieFile();
if (filepath.empty()) {
return true; // -norpccookiefile
return AuthCookieResult::Disabled; // -norpccookiefile
}
file.open(filepath.std_path());
if (!file.is_open())
return false;
std::getline(file, cookie);
if (!file.is_open()) {
return AuthCookieResult::Error;
}
std::getline(file, cookie_out);
file.close();
if (cookie_out)
*cookie_out = cookie;
return true;
return AuthCookieResult::Ok;
}
void DeleteAuthCookie()

View File

@@ -43,7 +43,7 @@ AuthCookieResult GenerateAuthCookie(const std::optional<fs::perms>& cookie_perms
std::string& pass);
/** Read the RPC authentication cookie from disk */
bool GetAuthCookie(std::string *cookie_out);
AuthCookieResult GetAuthCookie(std::string& cookie_out);
/** Delete RPC authentication cookie from disk */
void DeleteAuthCookie();
/** Parse JSON-RPC batch reply into a vector */

View File

@@ -139,11 +139,11 @@ class TestBitcoinCli(BitcoinTestFramework):
self.log.info("Test -stdinrpcpass option")
assert_equal(BLOCKS, self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input=password).getblockcount())
assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input='foo').echo)
assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword were specified', self.nodes[0].cli(f'-rpcuser={user}', '-stdinrpcpass', input='foo').echo)
self.log.info("Test -stdin and -stdinrpcpass")
assert_equal(['foo', 'bar'], self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input=f'{password}\nfoo\nbar').echo())
assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword', self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input='foo').echo)
assert_raises_process_error(1, 'Incorrect rpcuser or rpcpassword were specified', self.nodes[0].cli(f'-rpcuser={user}', '-stdin', '-stdinrpcpass', input='foo').echo)
self.log.info("Test connecting to a non-existing server")
assert_raises_process_error(1, "Could not connect to the server", self.nodes[0].cli('-rpcport=1').echo)
@@ -196,7 +196,14 @@ class TestBitcoinCli(BitcoinTestFramework):
self.nodes[0].replace_in_config([("#" + conf_rpcport, conf_rpcport)])
self.log.info("Test connecting with non-existing RPC cookie file")
assert_raises_process_error(1, "Could not locate RPC credentials", self.nodes[0].cli('-rpccookiefile=does-not-exist', '-rpcpassword=').echo)
assert_raises_process_error(1, "Failed to read cookie file and no rpcpassword was specified.", self.nodes[0].cli('-rpccookiefile=does-not-exist', '-rpcpassword=').echo)
self.log.info("Test connecting with neither cookie file, nor password")
assert_raises_process_error(1, "Cookie file was disabled via -norpccookiefile and no rpcpassword was specified.", self.nodes[0].cli("-norpccookiefile").echo)
assert_raises_process_error(1, "Cookie file was disabled via -norpccookiefile and no rpcpassword was specified.", self.nodes[0].cli("-norpccookiefile", "-rpcpassword=").echo)
self.log.info("Test connecting with invalid cookie file")
assert_raises_process_error(1, "Cookie file credentials were invalid and no rpcpassword was specified.", self.nodes[0].cli(f"-rpccookiefile={self.nodes[0].datadir_path / 'bitcoin.conf'}").echo)
self.log.info("Test connecting without RPC cookie file and with password arg")
assert_equal(BLOCKS, self.nodes[0].cli('-norpccookiefile', f'-rpcuser={user}', f'-rpcpassword={password}').getblockcount())

View File

@@ -31,7 +31,7 @@ class TestBitcoinIpcCli(BitcoinTestFramework):
if error is None:
assert_equal(result.stdout, '[\n "foo"\n]\n')
else:
assert_equal(result.stdout, error)
assert result.stdout.startswith(error), f"Output didn't start with the expected error {error!r}:\n{result.stdout}"
assert_equal(result.stderr, None)
assert_equal(result.returncode, 0 if error is None else 1)
@@ -40,7 +40,7 @@ class TestBitcoinIpcCli(BitcoinTestFramework):
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_auth_error = "error: Authorization failed: Incorrect rpcuser or rpcpassword were specified."
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"