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
16 changes: 16 additions & 0 deletions doc/JSON-RPC-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ major version via the `-deprecatedrpc=` command line option. The release notes
of a new major release come with detailed instructions on what RPC features
were deprecated and how to re-enable them temporarily.

## JSON-RPC 1.1 vs 2.0

The server recognizes [JSON-RPC v2.0](https://www.jsonrpc.org/specification) requests
and responds accordingly. A 2.0 request is identified by the presence of
`"jsonrpc": "2.0"` in the request body. If that key + value is not present in a request,
the legacy JSON-RPC v1.1 protocol is followed instead, which was the only available
protocol in previous releases.
Copy link
Member

Choose a reason for hiding this comment

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

nit: Now that this is merged, it could say "in 27.0 and prior releases." Otherwise, on 29.x it will read as-if 28.0 had it missing.


|| 1.1 | 2.0 |
|-|-|-|
| Request marker | `"version": "1.1"` (or none) | `"jsonrpc": "2.0"` |
| Response marker | (none) | `"jsonrpc": "2.0"` |
| `"error"` and `"result"` fields in response | both present | only one is present |
Copy link
Contributor

Choose a reason for hiding this comment

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

In commit "doc: add comments and release-notes for JSON-RPC 2.0" (756da6d1e67fba65a992dc0090ee8c0cfa26abe3)

Think this should also mention that responses contain "jsonrpc": "2.0" field. Or this could be an additional row in the table.

| HTTP codes in response | `200` unless there is any kind of RPC error (invalid parameters, method not found, etc) | Always `200` unless there is an actual HTTP server error (request parsing error, endpoint not found, etc) |
| Notifications: requests that get no reply | (not supported) | Supported for requests that exclude the "id" field |

## Security

The RPC interface allows other programs to control Bitcoin Core,
Expand Down
9 changes: 9 additions & 0 deletions doc/release-notes-27101.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
JSON-RPC
--------

The JSON-RPC server now recognizes JSON-RPC 2.0 requests and responds with
strict adherence to the specification (https://www.jsonrpc.org/specification):

- Returning HTTP "204 No Content" responses to JSON-RPC 2.0 notifications instead of full responses.
- Returning HTTP "200 OK" responses in all other cases, rather than 404 responses for unknown methods, 500 responses for invalid parameters, etc.
- Returning either "result" fields or "error" fields in JSON-RPC responses, rather than returning both fields with one field set to null.
Copy link
Member

Choose a reason for hiding this comment

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

nit instead of duplicating the section from doc/JSON-RPC-interface.md here, it seems better to link/refer to it. Otherwise, if the section is updated, this may go stale or must be updated at the same time.

8 changes: 4 additions & 4 deletions src/bitcoin-cli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ class AddrinfoRequestHandler : public BaseRequestHandler
}
addresses.pushKV("total", total);
result.pushKV("addresses_known", addresses);
return JSONRPCReplyObj(result, NullUniValue, 1);
return JSONRPCReplyObj(std::move(result), NullUniValue, /*id=*/1, JSONRPCVersion::V1_LEGACY);
}
};

Expand Down Expand Up @@ -371,7 +371,7 @@ class GetinfoRequestHandler: public BaseRequestHandler
}
result.pushKV("relayfee", batch[ID_NETWORKINFO]["result"]["relayfee"]);
result.pushKV("warnings", batch[ID_NETWORKINFO]["result"]["warnings"]);
return JSONRPCReplyObj(result, NullUniValue, 1);
return JSONRPCReplyObj(std::move(result), NullUniValue, /*id=*/1, JSONRPCVersion::V1_LEGACY);
}
};

Expand Down Expand Up @@ -623,7 +623,7 @@ class NetinfoRequestHandler : public BaseRequestHandler
}
}

return JSONRPCReplyObj(UniValue{result}, NullUniValue, 1);
return JSONRPCReplyObj(UniValue{result}, NullUniValue, /*id=*/1, JSONRPCVersion::V1_LEGACY);
}

const std::string m_help_doc{
Expand Down Expand Up @@ -709,7 +709,7 @@ class GenerateToAddressRequestHandler : public BaseRequestHandler
UniValue result(UniValue::VOBJ);
result.pushKV("address", address_str);
result.pushKV("blocks", reply.get_obj()["result"]);
return JSONRPCReplyObj(result, NullUniValue, 1);
return JSONRPCReplyObj(std::move(result), NullUniValue, /*id=*/1, JSONRPCVersion::V1_LEGACY);
}
protected:
std::string address_str;
Expand Down
66 changes: 55 additions & 11 deletions src/httprpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,11 @@ 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, const UniValue& objError, const UniValue& id)
static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCRequest& jreq)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: JSONErrorReply -> JSONRPCErrorReply, although it could be argued that it actually does write a JSON object in the response.

{
// Sending HTTP errors is a legacy JSON-RPC behavior.
Assume(jreq.m_json_version != JSONRPCVersion::V2);

// Send error reply from json-rpc error object
int nStatus = HTTP_INTERNAL_SERVER_ERROR;
int code = objError.find_value("code").getInt<int>();
Expand All @@ -84,7 +87,7 @@ static void JSONErrorReply(HTTPRequest* req, const UniValue& objError, const Uni
else if (code == RPC_METHOD_NOT_FOUND)
nStatus = HTTP_NOT_FOUND;

std::string strReply = JSONRPCReply(NullUniValue, objError, id);
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);
Expand Down Expand Up @@ -185,7 +188,7 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
// Set the URI
jreq.URI = req->GetURI();

std::string strReply;
UniValue reply;
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);
Expand All @@ -200,13 +203,23 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
req->WriteReply(HTTP_FORBIDDEN);
return false;
}
UniValue result = tableRPC.execute(jreq);

// Send reply
strReply = JSONRPCReply(result, NullUniValue, jreq.id);
// 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);

if (jreq.IsNotification()) {
// Even though we do execute notifications, we do not respond to them
req->WriteReply(HTTP_NO_CONTENT);
return true;
}

// array of requests
} else if (valRequest.isArray()) {
// Check authorization for each request's method
if (user_has_whitelist) {
for (unsigned int reqIdx = 0; reqIdx < valRequest.size(); reqIdx++) {
if (!valRequest[reqIdx].isObject()) {
Expand All @@ -223,18 +236,49 @@ static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req)
}
}
}
strReply = JSONRPCExecBatch(jreq, valRequest.get_array());

// Execute each request
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.
UniValue response;
try {
jreq.parse(valRequest[i]);
response = JSONRPCExec(jreq, /*catch_errors=*/true);
} catch (UniValue& e) {
response = JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version);
} catch (const std::exception& e) {
response = JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id, jreq.m_json_version);
}
Comment on lines +246 to +253
Copy link
Contributor

Choose a reason for hiding this comment

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

(Came across this block again while working on something else. It struck me that the exception handling looked redundant as we catch the same exact exception types inside JSONRPCExec(jreq, /*catch_errors=*/true). But the jreq.parse() call will throw on missing methods etc.

One could move out response = JSONRPCExec(jreq, /*catch_errors=*/true); to after the block, but then one would need to guard against using a jreq that failed to parse. So the current version of the block is probably preferable both in readability and efficiency).

if (!jreq.IsNotification()) {
reply.push_back(std::move(response));
}
}
// Return no response for an all-notification batch, but only if the
// batch request is non-empty. Technically according to the JSON-RPC
// 2.0 spec, an empty batch request should also return no response,
// However, if the batch request is empty, it means the request did
// not contain any JSON-RPC version numbers, so returning an empty
// response could break backwards compatibility with old RPC clients
// relying on previous behavior. Return an empty array instead of an
// 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;
}
}
else
throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");

req->WriteHeader("Content-Type", "application/json");
req->WriteReply(HTTP_OK, strReply);
} catch (const UniValue& objError) {
JSONErrorReply(req, objError, jreq.id);
req->WriteReply(HTTP_OK, reply.write() + "\n");
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

That's a reasonable comment but I kept the single character which is how it worked on master (see below). I see a few examples of \r\n in the codebase but its rare and I'm not sure how it's decided.

std::string JSONRPCReply(const UniValue& result, const UniValue& error, const UniValue& id)
{
UniValue reply = JSONRPCReplyObj(result, error, id);
return reply.write() + "\n";
}

Copy link
Contributor

Choose a reason for hiding this comment

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

In commit "rpc: refactor single/batch requests" (a64a2b7)

re: #27101 (comment)

HTTP headers use \r\n, but this line is generating the JSON object in the body of the response, after the headers. And the JSON could be written with either \r\n or \n or no trailing line break at all. I think it would probably make sense not to include any trailing line break, but existing code uses \n, so in a refactoring commit it seems best not to change this.

} catch (UniValue& e) {
JSONErrorReply(req, std::move(e), jreq);
return false;
} catch (const std::exception& e) {
JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq);
return false;
}
return true;
Expand Down
1 change: 1 addition & 0 deletions src/rpc/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
enum HTTPStatusCode
{
HTTP_OK = 200,
HTTP_NO_CONTENT = 204,
HTTP_BAD_REQUEST = 400,
HTTP_UNAUTHORIZED = 401,
HTTP_FORBIDDEN = 403,
Expand Down
63 changes: 49 additions & 14 deletions src/rpc/request.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@
*
* 1.0 spec: http://json-rpc.org/wiki/specification
* 1.2 spec: http://jsonrpc.org/historical/json-rpc-over-http.html
*
* If the server receives a request with the JSON-RPC 2.0 marker `{"jsonrpc": "2.0"}`
* then Bitcoin will respond with a strictly specified response.
* It will only return an HTTP error code if an actual HTTP error is encountered
* such as the endpoint is not found (404) or the request is not formatted correctly (500).
* Otherwise the HTTP code is always OK (200) and RPC errors will be included in the
* response body.
*
* 2.0 spec: https://www.jsonrpc.org/specification
*
* Also see http://www.simple-is-better.org/rpc/#differences-between-1-0-and-2-0
*/

UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params, const UniValue& id)
Expand All @@ -37,24 +48,25 @@ UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params,
return request;
}

UniValue JSONRPCReplyObj(const UniValue& result, const UniValue& error, const UniValue& id)
UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue> id, JSONRPCVersion jsonrpc_version)
{
UniValue reply(UniValue::VOBJ);
if (!error.isNull())
reply.pushKV("result", NullUniValue);
else
reply.pushKV("result", result);
reply.pushKV("error", error);
reply.pushKV("id", id);
// Add JSON-RPC version number field in v2 only.
if (jsonrpc_version == JSONRPCVersion::V2) reply.pushKV("jsonrpc", "2.0");

// Add both result and error fields in v1, even though one will be null.
// Omit the null field in v2.
if (error.isNull()) {
reply.pushKV("result", std::move(result));
if (jsonrpc_version == JSONRPCVersion::V1_LEGACY) reply.pushKV("error", NullUniValue);
} else {
if (jsonrpc_version == JSONRPCVersion::V1_LEGACY) reply.pushKV("result", NullUniValue);
reply.pushKV("error", std::move(error));
}
if (id.has_value()) reply.pushKV("id", std::move(id.value()));
return reply;
}

std::string JSONRPCReply(const UniValue& result, const UniValue& error, const UniValue& id)
{
UniValue reply = JSONRPCReplyObj(result, error, id);
return reply.write() + "\n";
}

UniValue JSONRPCError(int code, const std::string& message)
{
UniValue error(UniValue::VOBJ);
Expand Down Expand Up @@ -171,7 +183,30 @@ void JSONRPCRequest::parse(const UniValue& valRequest)
const UniValue& request = valRequest.get_obj();

// Parse id now so errors from here on will have the id
id = request.find_value("id");
if (request.exists("id")) {
id = request.find_value("id");
} else {
id = std::nullopt;
}

// Check for JSON-RPC 2.0 (default 1.1)
m_json_version = JSONRPCVersion::V1_LEGACY;
const UniValue& jsonrpc_version = request.find_value("jsonrpc");
if (!jsonrpc_version.isNull()) {
if (!jsonrpc_version.isStr()) {
throw JSONRPCError(RPC_INVALID_REQUEST, "jsonrpc field must be a string");
}
// The "jsonrpc" key was added in the 2.0 spec, but some older documentation
// incorrectly included {"jsonrpc":"1.0"} in a request object, so we
// maintain that for backwards compatibility.
Copy link
Contributor

Choose a reason for hiding this comment

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

In commit "rpc: identify JSON-RPC 2.0 requests" (2ca1460)

I think it would be a little clearer to say "continue to accept that" instead of "maintain that." Otherwise it sounds like we are trying to maintain incorrectly including the field, not just allowing it if it is specified.

if (jsonrpc_version.get_str() == "1.0") {
m_json_version = JSONRPCVersion::V1_LEGACY;
} else if (jsonrpc_version.get_str() == "2.0") {
m_json_version = JSONRPCVersion::V2;
} else {
throw JSONRPCError(RPC_INVALID_REQUEST, "JSON-RPC version not supported");
}
}

// Parse method
const UniValue& valMethod{request.find_value("method")};
Expand Down
13 changes: 10 additions & 3 deletions src/rpc/request.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
#define BITCOIN_RPC_REQUEST_H

#include <any>
#include <optional>
#include <string>

#include <univalue.h>

enum class JSONRPCVersion {
V1_LEGACY,
V2
};

UniValue JSONRPCRequestObj(const std::string& strMethod, const UniValue& params, const UniValue& id);
UniValue JSONRPCReplyObj(const UniValue& result, const UniValue& error, const UniValue& id);
std::string JSONRPCReply(const UniValue& result, const UniValue& error, const UniValue& id);
UniValue JSONRPCReplyObj(UniValue result, UniValue error, std::optional<UniValue> id, JSONRPCVersion jsonrpc_version);
UniValue JSONRPCError(int code, const std::string& message);

/** Generate a new RPC authentication cookie and write it to disk */
Expand All @@ -28,16 +33,18 @@ std::vector<UniValue> JSONRPCProcessBatchReply(const UniValue& in);
class JSONRPCRequest
{
public:
UniValue id;
std::optional<UniValue> id = UniValue::VNULL;
std::string strMethod;
UniValue params;
enum Mode { EXECUTE, GET_HELP, GET_ARGS } mode = EXECUTE;
std::string URI;
std::string authUser;
std::string peerAddr;
std::any context;
JSONRPCVersion m_json_version = JSONRPCVersion::V1_LEGACY;

void parse(const UniValue& valRequest);
[[nodiscard]] bool IsNotification() const { return !id.has_value() && m_json_version == JSONRPCVersion::V2; };
};

#endif // BITCOIN_RPC_REQUEST_H
42 changes: 14 additions & 28 deletions src/rpc/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -358,36 +358,22 @@ bool IsDeprecatedRPCEnabled(const std::string& method)
return find(enabled_methods.begin(), enabled_methods.end(), method) != enabled_methods.end();
}

static UniValue JSONRPCExecOne(JSONRPCRequest jreq, const UniValue& req)
{
UniValue rpc_result(UniValue::VOBJ);

try {
jreq.parse(req);

UniValue result = tableRPC.execute(jreq);
rpc_result = JSONRPCReplyObj(result, NullUniValue, jreq.id);
}
catch (const UniValue& objError)
{
rpc_result = JSONRPCReplyObj(NullUniValue, objError, jreq.id);
}
catch (const std::exception& e)
{
rpc_result = JSONRPCReplyObj(NullUniValue,
JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
UniValue JSONRPCExec(const JSONRPCRequest& jreq, bool catch_errors)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would have gone the opposite way and called it throw_errors since it is an.. exception.. to maintain legacy behavior. Sorry for not catching that earlier.

Copy link
Contributor

Choose a reason for hiding this comment

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

re: #27101 (comment)

Would have gone the opposite way and called it throw_errors since it is an.. exception.. to maintain legacy behavior. Sorry for not catching that earlier.

I think either way is fine, but catch_errors does seem more literally correct since the argument is just controlling whether the exceptions will be caught. Errors will be still be thrown regardless.

{
UniValue result;
if (catch_errors) {
try {
result = tableRPC.execute(jreq);
} catch (UniValue& e) {
return JSONRPCReplyObj(NullUniValue, std::move(e), jreq.id, jreq.m_json_version);
} catch (const std::exception& e) {
return JSONRPCReplyObj(NullUniValue, JSONRPCError(RPC_MISC_ERROR, e.what()), jreq.id, jreq.m_json_version);
}
} else {
result = tableRPC.execute(jreq);
}

return rpc_result;
}

std::string JSONRPCExecBatch(const JSONRPCRequest& jreq, const UniValue& vReq)
{
UniValue ret(UniValue::VARR);
for (unsigned int reqIdx = 0; reqIdx < vReq.size(); reqIdx++)
ret.push_back(JSONRPCExecOne(jreq, vReq[reqIdx]));

return ret.write() + "\n";
return JSONRPCReplyObj(std::move(result), NullUniValue, jreq.id, jreq.m_json_version);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/rpc/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ extern CRPCTable tableRPC;
void StartRPC();
void InterruptRPC();
void StopRPC();
std::string JSONRPCExecBatch(const JSONRPCRequest& jreq, const UniValue& vReq);
UniValue JSONRPCExec(const JSONRPCRequest& jreq, bool catch_errors);

// Drop witness when serializing for RPC?
bool RPCSerializationWithoutWitness();
Expand Down
4 changes: 2 additions & 2 deletions src/rpc/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ std::string HelpExampleCliNamed(const std::string& methodname, const RPCArgList&

std::string HelpExampleRpc(const std::string& methodname, const std::string& args)
{
return "> curl --user myusername --data-binary '{\"jsonrpc\": \"1.0\", \"id\": \"curltest\", "
return "> curl --user myusername --data-binary '{\"jsonrpc\": \"2.0\", \"id\": \"curltest\", "
"\"method\": \"" + methodname + "\", \"params\": [" + args + "]}' -H 'content-type: text/plain;' http://127.0.0.1:8332/\n";
}

Expand All @@ -172,7 +172,7 @@ std::string HelpExampleRpcNamed(const std::string& methodname, const RPCArgList&
params.pushKV(param.first, param.second);
}

return "> curl --user myusername --data-binary '{\"jsonrpc\": \"1.0\", \"id\": \"curltest\", "
return "> curl --user myusername --data-binary '{\"jsonrpc\": \"2.0\", \"id\": \"curltest\", "
"\"method\": \"" + methodname + "\", \"params\": " + params.write() + "}' -H 'content-type: text/plain;' http://127.0.0.1:8332/\n";
}

Expand Down
Loading