mirror of
https://github.com/XRPLF/rippled.git
synced 2026-10-22 03:27:08 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b515af4d4e | ||
|
|
cf495fde86 | ||
|
|
00bf88afad | ||
|
|
f45dab46d1 | ||
|
|
b87ad0f08e | ||
|
|
50c9e6335e | ||
|
|
78709c1d09 | ||
|
|
b67266aad4 |
No files matched your search
@@ -177,6 +177,7 @@ test.server > xrpld.core
|
||||
test.server > xrpld.rpc
|
||||
test.server > xrpl.json
|
||||
test.server > xrpl.protocol
|
||||
test.server > xrpl.resource
|
||||
test.server > xrpl.server
|
||||
test.unit_test > xrpl.basics
|
||||
test.unit_test > xrpl.protocol
|
||||
|
||||
@@ -24,6 +24,14 @@ This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-R
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- The `ripplerpc` request field, which selects the shape of the JSON-RPC reply envelope, is now validated, and a value that is not exactly `"1.0"`, `"2.0"` or `"3.0"` is rejected. This reaches the JSON-RPC transport at every API version. It does not reach a WebSocket session, which reads the field only to echo it back. A request sending `"2"`, `" 2.0"`, `"2.00"`, `"02.0"`, `"2.0.0"`, `"10.0"` or any other text, such as `"abc"` or `"x2"`, gets a working reply today. After this ships, such a request sent alone gets HTTP 400 with `ripplerpc is not a supported version`, charged as a malformed request, and such an entry of a `"method": "batch"` body gets that error in its own reply while the batch answers 200. A client that spells the version loosely must therefore be corrected to one of the three exact values, or omit the field. Previously the value was compared as a string, which both accepted values that name no version and ordered multi-digit versions incorrectly: `"abc"` and `"x2"` sorted above `"3.0"` and so selected version 3, and `"10.0"` sorted below `"2.0"` and so selected version 1. Requests that send one of the three supported values, or omit the field, are unaffected.
|
||||
|
||||
### Additions
|
||||
|
||||
- A JSON-RPC (HTTP) request may send its parameters as an object, `"params": {"account": "r..."}`, as well as the array of one object that was already accepted. This reaches every API version: such a request was previously rejected with HTTP 400 and `params unparsable`, and is now served. A request that already sends the array form is unaffected. An entry of a `"method": "batch"` request that carries a by-name `params` object is read for `api_version` and credentials from that object. Previously such an entry had its `api_version` read from its top level and its credentials ignored, since credentials were read only from an array-form `params`.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
- A request echoed back in an error reply now has every credential-bearing field masked: `admin_password`, `admin_user`, `passphrase`, `password`, `secret`, `seed`, `seed_hex`, `url_password`, `url_username` and `username`. Nesting no longer matters, so a credential inside `params` is masked too. The same masking is applied to every request and reply written to the log, and it covers six further names that only a reply carries: `master_key`, `master_seed`, `master_seed_hex`, `validation_key`, `validation_private_key` and `validation_seed`, which is how `wallet_propose` and `validation_create` used to write a live private key to the log. A request or reply written to the log is truncated at 10,000 characters. An `[rpc_startup]` line that does not parse is reported by its position in the section and the parser's message, never by its text, and no command runs for it.
|
||||
@@ -31,6 +39,12 @@ This version is supported by all `xrpld` versions. For WebSocket and HTTP JSON-R
|
||||
- A WebSocket frame that does not parse, or exceeds the request size limit, is answered `{"type": "error", "error": "jsonInvalid", "size": <bytes>}`. The frame's body is reported by size rather than echoed back in a `value` member, since a body that does not parse has no fields to mask. A client that read `value` gets `size` instead.
|
||||
- Four error codes that named no HTTP status of their own, and so answered 200 on a reply reporting an error, now name one: `actMalformed`, `alreadyMultisig` and `alreadySingleSig` answer 400, and `actNotFound` answers 404. **No shipped envelope reports these four.** A request sending `ripplerpc: "3.0"` still receives 200 for all four, as it always has, so `account_info` on a malformed account or one the ledger does not hold answers 200 exactly as before.
|
||||
- `submit`, `simulate`, `transaction_entry`, `ledger_entry` and `ledger_accept`: Errors from these methods now include `error_code` and `error_message` alongside the `error` token, as every other method already did. Each error now answers the status its code names: 400 for a malformed request, 404 for `transactionNotFound`, 500 for an internal failure, and 501 for `notYetImplemented` and `notStandAlone`. That status change reaches only a request sending `ripplerpc: "3.0"`, which is the envelope that derives the status from the error. With `ripplerpc` `"1.0"` the status stays 200 and the two new members appear beside `error`; with `"2.0"` the status stays 200, `error_code` appears, and the `code` and `message` members carry the code and the message rather than null, since that envelope copies them from `error_code` and `error_message` and drops `error_message`.
|
||||
- A reply reporting HTTP 402 or 502 now carries a status line. Those two statuses named no case in the switch that writes one, so such a reply began with a header instead and did not parse as an HTTP response at all. Both are reachable at any API version with `ripplerpc: "3.0"`, which derives the status from the error code: 402 through `highFee` from `sign`, `sign_for` or `submit` with a low `fee_mult_max`, and 502 through `dbDeserialization` from `tx`. The eleven statuses that already named a case report the same phrase they always have.
|
||||
- An error reply to a request sending `ripplerpc: "2.0"` or `"3.0"` no longer carries a stray `"error_message": null` beside the error it reports. The member appeared only when the `Server` log partition was set to debug or lower, because the log statement read `error_message` after the reply had renamed it to `message`, and reading it put it back as null. So the reply a client received depended on the server's log level, and the log line itself printed an empty message. Both are fixed.
|
||||
- A body the server rejects before it reads a request out of it now says which of four things was wrong. A body over the size limit answers `Request is too large`. A body that parses to `{}`, `[]` or `null` answers `Request is empty`. A body that parses to a non-empty array answers `Request is not a JSON object`; that is the only other document the parser accepts at the top level. All three previously answered `Unable to parse request: ` with nothing after the colon, the parser having recorded no error for them. Any other body does not parse, which includes one that is only whitespace and one whose top-level value is a string, number or boolean; it answers `Unable to parse request: ` followed by the parser's own reason, as it did before. The status is 400 for all four, as before, and this reaches every API version.
|
||||
- `batch`: An entry that is not identified through a secure gateway no longer clears the connection's `X-User` and forwarded-for values for the entries after it, so every entry of one body reports the role and username it would have reported on its own.
|
||||
- `batch`: Every entry of a `"method": "batch"` request is now charged against the sender's resource allowance, including one rejected before it reaches a handler, and the request stops at the first entry the connection is too loaded to serve. A reply array can therefore be shorter than the request array, and its last element depends on where the batch stopped. An entry that is not a JSON object, or names an API version the server does not serve, is charged before its role is known and answered with its own rejection; if that charge took the connection over the drop threshold, the batch stops there and that rejection is the last answer. Every other entry met over the threshold is answered `Server is overloaded` and nothing follows. A client should read any reply array shorter than its request as a connection over the drop threshold, whatever the last element says. A well-behaved client is unaffected; one that sends thousands of entries in a single body no longer gets every one of them answered.
|
||||
- A body the server rejects before it reads a request out of it is now charged against the sender's resource allowance, as a malformed request already was. Five conditions were free: a body over the size limit, one that does not parse, one carrying no document, one that is not a JSON object, and a `"method": "batch"` naming no entry array. The answer to each is unchanged. A well-behaved client is unaffected; one that repeats such a body exhausts its allowance and is refused the next request a handler would have served. A WebSocket frame that does not parse, or exceeds the request size limit, is charged the same way, and a connection that sends only such frames is closed once it crosses the drop threshold.
|
||||
|
||||
## XRP Ledger server version 3.5.0
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <xrpl/json/json_value.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
|
||||
@@ -164,7 +165,35 @@ enum ErrorCodeI {
|
||||
// ledger_entry, API version 1 only
|
||||
RpcUnknownOption = 109,
|
||||
|
||||
RpcLast = RpcUnknownOption, // rpcLAST should always equal the last code.
|
||||
// ledger_entry field validation, one code per malformed field. The ledger_entry helpers report
|
||||
// invalidParams (31) for all of them, so each token needs a row of its own. Alphabetical by
|
||||
// token here only: the enum is append-only once a code ships.
|
||||
//
|
||||
// `malformedIssue` duplicates `issueMalformed` (93): same message, same status, two tokens,
|
||||
// both on the wire already. Do not add a third spelling.
|
||||
RpcMalformedAccount = 110,
|
||||
RpcMalformedAddress = 111,
|
||||
RpcMalformedAuthorized = 112,
|
||||
RpcMalformedAuthorizedCredentials = 113,
|
||||
RpcMalformedBridgeAccount = 114,
|
||||
RpcMalformedBroker = 115,
|
||||
RpcMalformedCurrency = 116,
|
||||
RpcMalformedDirRoot = 117,
|
||||
RpcMalformedDocumentID = 118,
|
||||
RpcMalformedIssue = 119,
|
||||
RpcMalformedIssuingChainDoor = 120,
|
||||
RpcMalformedLockingChainDoor = 121,
|
||||
RpcMalformedMPTIssuanceID = 122,
|
||||
RpcMalformedMPTokenIssuance = 123,
|
||||
RpcMalformedOwner = 124,
|
||||
RpcMalformedSeq = 125,
|
||||
RpcMalformedSponsee = 126,
|
||||
RpcMalformedSponsor = 127,
|
||||
RpcMalformedXChainOwnedClaimID = 128,
|
||||
RpcMalformedXChainOwnedCreateAccountClaimID = 129,
|
||||
|
||||
// RpcLast should always equal the last code.
|
||||
RpcLast = RpcMalformedXChainOwnedCreateAccountClaimID,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -217,6 +246,18 @@ struct ErrorInfo
|
||||
ErrorInfo const&
|
||||
getErrorInfo(ErrorCodeI code);
|
||||
|
||||
/**
|
||||
* Returns the error code that owns @p token.
|
||||
*
|
||||
* A linear scan over views measured at compile time, run once per error reply.
|
||||
* A duplicate token is a build error, so the answer is never ambiguous.
|
||||
*
|
||||
* @param token The error token to resolve.
|
||||
* @return The code the table gives @p token, or RpcUnknown if no row names it.
|
||||
*/
|
||||
ErrorCodeI
|
||||
codeForToken(std::string_view token);
|
||||
|
||||
/**
|
||||
* Add or update the json update to reflect the error code.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <xrpl/json/json_forwards.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl::rpc {
|
||||
@@ -17,4 +19,25 @@ namespace xrpl::rpc {
|
||||
*/
|
||||
inline constexpr std::string_view kJsonRpcVersion{"2.0"};
|
||||
|
||||
/**
|
||||
* Codes for the `code` member of a JSON-RPC error object.
|
||||
*
|
||||
* The specification reserves -32768 to -32000 for the protocol and leaves
|
||||
* -32000 to -32099 of it to the implementation.
|
||||
*
|
||||
* kJsonRpcServerError is the code for an error an XRPL handler reports.
|
||||
*
|
||||
* The codes from kJsonRpcServerOverloaded on lie outside the
|
||||
* implementation-defined sub-range, which the specification does not allow.
|
||||
* They are the codes every shipped version reports, so moving one breaks the
|
||||
* clients matching on it.
|
||||
*/
|
||||
inline constexpr json::Int kJsonRpcServerError{-32000};
|
||||
inline constexpr json::Int kJsonRpcInvalidRequest{-32600};
|
||||
inline constexpr json::Int kJsonRpcMethodNotFound{-32601};
|
||||
inline constexpr json::Int kJsonRpcInvalidParams{-32602};
|
||||
inline constexpr json::Int kJsonRpcServerOverloaded{-32604};
|
||||
inline constexpr json::Int kJsonRpcForbidden{-32605};
|
||||
inline constexpr json::Int kJsonRpcWrongVersion{-32606};
|
||||
|
||||
} // namespace xrpl::rpc
|
||||
@@ -13,6 +13,8 @@ namespace xrpl {
|
||||
* credential this library cannot mask, so the caller logs it masked.
|
||||
*
|
||||
* A 401 with an empty body is answered with the fixed authentication page.
|
||||
* The status line carries the phrase Beast's registry gives @p nStatus, except
|
||||
* for 401 and 503, which carry a phrase of this server's own.
|
||||
*
|
||||
* @param nStatus The HTTP status code.
|
||||
* @param strMsg The body.
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
#include <xrpl/protocol/jss.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace xrpl {
|
||||
namespace rpc {
|
||||
@@ -116,6 +118,26 @@ static constexpr ErrorInfo kUnorderedErrorInfos[]{
|
||||
{RpcMalformedRequest, "malformedRequest", "Request is malformed.", 400},
|
||||
{RpcNotStandAlone, "notStandAlone", "Server is not running stand-alone.", 501},
|
||||
{RpcUnknownOption, "unknownOption", "Unknown option.", 400},
|
||||
{RpcMalformedAccount, "malformedAccount", "Account is malformed.", 400},
|
||||
{RpcMalformedAddress, "malformedAddress", "Address is malformed.", 400},
|
||||
{RpcMalformedAuthorized, "malformedAuthorized", "Authorized is malformed.", 400},
|
||||
{RpcMalformedAuthorizedCredentials, "malformedAuthorizedCredentials", "AuthorizedCredentials is malformed.", 400},
|
||||
{RpcMalformedBridgeAccount, "malformedBridgeAccount", "BridgeAccount is malformed.", 400},
|
||||
{RpcMalformedBroker, "malformedBroker", "Broker is malformed.", 400},
|
||||
{RpcMalformedCurrency, "malformedCurrency", "Currency is malformed.", 400},
|
||||
{RpcMalformedDirRoot, "malformedDirRoot", "DirRoot is malformed.", 400},
|
||||
{RpcMalformedDocumentID, "malformedDocumentID", "DocumentID is malformed.", 400},
|
||||
{RpcMalformedIssue, "malformedIssue", "Issue is malformed.", 400},
|
||||
{RpcMalformedIssuingChainDoor, "malformedIssuingChainDoor", "IssuingChainDoor is malformed.", 400},
|
||||
{RpcMalformedLockingChainDoor, "malformedLockingChainDoor", "LockingChainDoor is malformed.", 400},
|
||||
{RpcMalformedMPTIssuanceID, "malformedMPTIssuanceID", "MPTIssuanceID is malformed.", 400},
|
||||
{RpcMalformedMPTokenIssuance, "malformedMPTokenIssuance", "MPTokenIssuance is malformed.", 400},
|
||||
{RpcMalformedOwner, "malformedOwner", "Owner is malformed.", 400},
|
||||
{RpcMalformedSeq, "malformedSeq", "Seq is malformed.", 400},
|
||||
{RpcMalformedSponsee, "malformedSponsee", "Sponsee is malformed.", 400},
|
||||
{RpcMalformedSponsor, "malformedSponsor", "Sponsor is malformed.", 400},
|
||||
{RpcMalformedXChainOwnedClaimID, "malformedXChainOwnedClaimID", "XChainOwnedClaimID is malformed.", 400},
|
||||
{RpcMalformedXChainOwnedCreateAccountClaimID, "malformedXChainOwnedCreateAccountClaimID", "XChainOwnedCreateAccountClaimID is malformed.", 400},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
@@ -131,7 +153,7 @@ sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array<ErrorInfo, M>
|
||||
if (info.code <= RpcSuccess || info.code > RpcLast)
|
||||
throw(std::out_of_range("Invalid error_code_i"));
|
||||
|
||||
// The first valid code follows rpcSUCCESS immediately.
|
||||
// The first valid code follows RpcSuccess immediately.
|
||||
static_assert(RpcSuccess == 0, "Unexpected error_code_i layout.");
|
||||
int const index{info.code - 1};
|
||||
|
||||
@@ -142,10 +164,10 @@ sortErrorInfos(ErrorInfo const (&unordered)[N]) -> std::array<ErrorInfo, M>
|
||||
}
|
||||
|
||||
// Verify that all entries are filled in starting with 1 and proceeding
|
||||
// to rpcLAST.
|
||||
// to RpcLast.
|
||||
//
|
||||
// It's okay for there to be missing entries; they will contain the code
|
||||
// rpcUNKNOWN. But other than that all entries should match their index.
|
||||
// RpcUnknown. But other than that all entries should match their index.
|
||||
int codeCount{0};
|
||||
int expect{RpcBadSyntax - 1};
|
||||
for (ErrorInfo const& info : ret)
|
||||
@@ -170,6 +192,68 @@ constexpr auto kSortedErrorInfos{sortErrorInfos<RpcLast>(kUnorderedErrorInfos)};
|
||||
|
||||
constexpr ErrorInfo kUnknownError;
|
||||
|
||||
/**
|
||||
* Asserts that no two rows of @p sorted name the same token.
|
||||
*
|
||||
* A duplicate is then a build error rather than a reply reporting the wrong
|
||||
* `error_code`. `consteval` admits no run-time call, so the two `throw`
|
||||
* statements are how a constant evaluation fails and are otherwise unreachable.
|
||||
*
|
||||
* @param sorted The table, ordered by code, with a gap for every unused code.
|
||||
* @return True. It reports failure by failing to be a constant expression.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
consteval bool
|
||||
tokensAreUnique(std::array<ErrorInfo, N> const& sorted)
|
||||
{
|
||||
for (std::size_t i{0}; i < N; ++i)
|
||||
{
|
||||
// Gaps in the table name no error, so they name no token either.
|
||||
if (sorted[i].code == RpcUnknown)
|
||||
continue;
|
||||
|
||||
std::string_view const token{sorted[i].token.cStr()};
|
||||
if (token.empty())
|
||||
throw(std::invalid_argument("Empty token in list"));
|
||||
|
||||
for (std::size_t j{i + 1}; j < N; ++j)
|
||||
{
|
||||
if (sorted[j].code != RpcUnknown && std::string_view{sorted[j].token.cStr()} == token)
|
||||
throw(std::invalid_argument("Duplicate token in list"));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static_assert(tokensAreUnique(kSortedErrorInfos), "Duplicate token in list");
|
||||
|
||||
/**
|
||||
* The tokens of @p sorted, measured at compile time.
|
||||
*
|
||||
* A lookup compares a caller's token against these rather than against the
|
||||
* table's own `char const*`, which it would have to measure first: comparing a
|
||||
* view with a pointer calls strlen on every row the scan passes. Built from the
|
||||
* same table, so the two cannot name different tokens.
|
||||
*
|
||||
* @param sorted The table, ordered by code, with a gap for every unused code.
|
||||
* @return One view per row, the empty view for a gap. No token a caller can
|
||||
* pass is equal to the empty view.
|
||||
*/
|
||||
template <std::size_t N>
|
||||
consteval std::array<std::string_view, N>
|
||||
tokenViews(std::array<ErrorInfo, N> const& sorted)
|
||||
{
|
||||
std::array<std::string_view, N> ret{};
|
||||
for (std::size_t i{0}; i < N; ++i)
|
||||
{
|
||||
if (sorted[i].code != RpcUnknown)
|
||||
ret[i] = std::string_view{sorted[i].token.cStr()};
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
constexpr auto kSortedTokens{tokenViews(kSortedErrorInfos)};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -200,6 +284,21 @@ getErrorInfo(ErrorCodeI code)
|
||||
return detail::kSortedErrorInfos[code - 1];
|
||||
}
|
||||
|
||||
ErrorCodeI
|
||||
codeForToken(std::string_view token)
|
||||
{
|
||||
if (token.empty())
|
||||
return RpcUnknown;
|
||||
|
||||
for (std::size_t i{0}; i < detail::kSortedTokens.size(); ++i)
|
||||
{
|
||||
// A gap holds the empty view, which the token is not, so a gap names no code here either.
|
||||
if (detail::kSortedTokens[i] == token)
|
||||
return detail::kSortedErrorInfos[i].code;
|
||||
}
|
||||
return RpcUnknown;
|
||||
}
|
||||
|
||||
json::Value
|
||||
makeError(ErrorCodeI code)
|
||||
{
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
|
||||
#include <boost/beast/http/status.hpp>
|
||||
|
||||
#include <ctime>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
namespace xrpl {
|
||||
@@ -72,42 +75,29 @@ httpReply(int nStatus, std::string const& content, json::Output const& output, b
|
||||
return;
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(bugprone-switch-missing-default-case)
|
||||
switch (nStatus)
|
||||
{
|
||||
// The status every successful reply carries, so a literal rather than a format call.
|
||||
case 200:
|
||||
output("HTTP/1.1 200 OK\r\n");
|
||||
break;
|
||||
case 202:
|
||||
output("HTTP/1.1 202 Accepted\r\n");
|
||||
break;
|
||||
case 400:
|
||||
output("HTTP/1.1 400 Bad Request\r\n");
|
||||
break;
|
||||
// Two statuses this server phrases itself rather than taking from the registry.
|
||||
case 401:
|
||||
output("HTTP/1.1 401 Authorization Required\r\n");
|
||||
break;
|
||||
case 403:
|
||||
output("HTTP/1.1 403 Forbidden\r\n");
|
||||
break;
|
||||
case 404:
|
||||
output("HTTP/1.1 404 Not Found\r\n");
|
||||
break;
|
||||
case 405:
|
||||
output("HTTP/1.1 405 Method Not Allowed\r\n");
|
||||
break;
|
||||
case 429:
|
||||
output("HTTP/1.1 429 Too Many Requests\r\n");
|
||||
break;
|
||||
case 500:
|
||||
output("HTTP/1.1 500 Internal Server Error\r\n");
|
||||
break;
|
||||
case 501:
|
||||
output("HTTP/1.1 501 Not Implemented\r\n");
|
||||
break;
|
||||
case 503:
|
||||
output("HTTP/1.1 503 Server is overloaded\r\n");
|
||||
break;
|
||||
default:
|
||||
// A reply whose first line is a header is not an HTTP response. Beast knows the whole
|
||||
// registry, so a status the error table gains needs no case here.
|
||||
output(
|
||||
std::format(
|
||||
"HTTP/1.1 {} {}\r\n",
|
||||
nStatus,
|
||||
boost::beast::http::obsolete_reason(
|
||||
static_cast<boost::beast::http::status>(nStatus))));
|
||||
break;
|
||||
}
|
||||
|
||||
output(getHTTPHeaderTimestamp());
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -71,3 +71,48 @@ TEST(ErrorCodes, every_code_names_an_http_status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, code_for_token_resolves_known_tokens)
|
||||
{
|
||||
EXPECT_EQ(rpc::codeForToken("invalidParams"), RpcInvalidParams);
|
||||
EXPECT_EQ(rpc::codeForToken("malformedSeq"), RpcMalformedSeq);
|
||||
|
||||
// The first and last entries, so a probe that wraps or stops short is caught.
|
||||
EXPECT_EQ(rpc::codeForToken("badSyntax"), RpcBadSyntax);
|
||||
EXPECT_EQ(
|
||||
rpc::codeForToken("malformedXChainOwnedCreateAccountClaimID"),
|
||||
RpcMalformedXChainOwnedCreateAccountClaimID);
|
||||
static_assert(RpcMalformedXChainOwnedCreateAccountClaimID == RpcLast);
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, code_for_token_rejects_unnamed_tokens)
|
||||
{
|
||||
EXPECT_EQ(rpc::codeForToken("notATokenAnyEntryNames"), RpcUnknown);
|
||||
EXPECT_EQ(rpc::codeForToken(""), RpcUnknown);
|
||||
|
||||
// Tokens are matched whole, not by prefix, and the comparison is case-sensitive.
|
||||
EXPECT_EQ(rpc::codeForToken("invalidParam"), RpcUnknown);
|
||||
EXPECT_EQ(rpc::codeForToken("invalidParamsX"), RpcUnknown);
|
||||
EXPECT_EQ(rpc::codeForToken("invalidparams"), RpcUnknown);
|
||||
|
||||
// The placeholder that gaps in the table report names no error, so it resolves to nothing.
|
||||
EXPECT_EQ(rpc::codeForToken("unknown"), RpcUnknown);
|
||||
}
|
||||
|
||||
TEST(ErrorCodes, every_token_round_trips_to_its_own_code)
|
||||
{
|
||||
// The views the lookup scans and the rows it answers from are two arrays built from one table.
|
||||
// Every row's token resolving to that row's code is what proves the two are index-aligned,
|
||||
// which no static assertion checks.
|
||||
for (int i = RpcBadSyntax; i <= RpcLast; ++i)
|
||||
{
|
||||
auto const code = static_cast<ErrorCodeI>(i);
|
||||
auto const& info = rpc::getErrorInfo(code);
|
||||
|
||||
// Gaps report the placeholder, which is covered above.
|
||||
if (info.code == RpcUnknown)
|
||||
continue;
|
||||
|
||||
EXPECT_EQ(rpc::codeForToken(info.token.cStr()), code) << "token: " << info.token.cStr();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#include <xrpl/server/detail/JSONRPCUtil.h>
|
||||
|
||||
#include <xrpl/beast/utility/Journal.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
using namespace xrpl;
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* The reply httpReply writes, collected as one string.
|
||||
*
|
||||
* @param status The HTTP status to write a reply for.
|
||||
* @return The whole reply, headers and body.
|
||||
*/
|
||||
std::string
|
||||
reply(int status)
|
||||
{
|
||||
std::string out;
|
||||
beast::Journal const journal{beast::Journal::getNullSink()};
|
||||
httpReply(status, "{}", [&out](std::string_view s) { out += s; }, journal);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* The reply's status line, without the trailing CRLF.
|
||||
*
|
||||
* @param reply A whole HTTP reply.
|
||||
* @return The first line, or the whole reply when it holds no CRLF.
|
||||
*/
|
||||
std::string_view
|
||||
statusLine(std::string const& reply)
|
||||
{
|
||||
auto const end = reply.find("\r\n");
|
||||
return std::string_view{reply}.substr(0, end == std::string::npos ? reply.size() : end);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(JSONRPCUtil, status_line_names_the_status)
|
||||
{
|
||||
// Every status this server sends but the two the last test names. The phrase comes from the
|
||||
// registry but for the two below.
|
||||
EXPECT_EQ(statusLine(reply(200)), "HTTP/1.1 200 OK");
|
||||
EXPECT_EQ(statusLine(reply(202)), "HTTP/1.1 202 Accepted");
|
||||
EXPECT_EQ(statusLine(reply(400)), "HTTP/1.1 400 Bad Request");
|
||||
EXPECT_EQ(statusLine(reply(403)), "HTTP/1.1 403 Forbidden");
|
||||
EXPECT_EQ(statusLine(reply(404)), "HTTP/1.1 404 Not Found");
|
||||
EXPECT_EQ(statusLine(reply(405)), "HTTP/1.1 405 Method Not Allowed");
|
||||
EXPECT_EQ(statusLine(reply(429)), "HTTP/1.1 429 Too Many Requests");
|
||||
EXPECT_EQ(statusLine(reply(500)), "HTTP/1.1 500 Internal Server Error");
|
||||
EXPECT_EQ(statusLine(reply(501)), "HTTP/1.1 501 Not Implemented");
|
||||
|
||||
// The two whose phrase is this server's own, not the registry's.
|
||||
EXPECT_EQ(statusLine(reply(401)), "HTTP/1.1 401 Authorization Required");
|
||||
EXPECT_EQ(statusLine(reply(503)), "HTTP/1.1 503 Server is overloaded");
|
||||
}
|
||||
|
||||
TEST(JSONRPCUtil, every_status_the_error_table_names_gets_a_status_line)
|
||||
{
|
||||
// httpReply sees a status, not the table, so only this can check every status the table names.
|
||||
std::set<int> statuses;
|
||||
for (int i = RpcBadSyntax; i <= RpcLast; ++i)
|
||||
{
|
||||
auto const& info = rpc::getErrorInfo(static_cast<ErrorCodeI>(i));
|
||||
|
||||
// Gaps in the table name no error, so they report no status of their own.
|
||||
if (info.code == RpcUnknown)
|
||||
continue;
|
||||
|
||||
statuses.insert(info.httpStatus);
|
||||
}
|
||||
EXPECT_FALSE(statuses.empty());
|
||||
|
||||
for (int const status : statuses)
|
||||
{
|
||||
auto const line = std::string{statusLine(reply(status))};
|
||||
auto const expectedPrefix = "HTTP/1.1 " + std::to_string(status) + " ";
|
||||
EXPECT_TRUE(line.starts_with(expectedPrefix)) << "status: " << status << ", line: " << line;
|
||||
|
||||
// A placeholder phrase means the table names something that is not a status.
|
||||
EXPECT_GT(line.size(), expectedPrefix.size()) << "status: " << status;
|
||||
EXPECT_EQ(line.find("unknown-status"), std::string::npos) << "status: " << status;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(JSONRPCUtil, statuses_without_a_case_take_the_registry_phrase)
|
||||
{
|
||||
// The two statuses the error table names and the switch spells no case for.
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcHighFee), 402);
|
||||
EXPECT_EQ(statusLine(reply(402)), "HTTP/1.1 402 Payment Required");
|
||||
|
||||
EXPECT_EQ(rpc::errorCodeHttpStatus(RpcDbDeserialization), 502);
|
||||
EXPECT_EQ(statusLine(reply(502)), "HTTP/1.1 502 Bad Gateway");
|
||||
|
||||
// A number the registry does not know still gets a line, with the placeholder phrase the table
|
||||
// test above refuses.
|
||||
EXPECT_EQ(statusLine(reply(999)), "HTTP/1.1 999 <unknown-status>");
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <xrpl/protocol/ApiVersion.h>
|
||||
#include <xrpl/protocol/BuildInfo.h>
|
||||
#include <xrpl/protocol/ErrorCodes.h>
|
||||
#include <xrpl/protocol/JsonRpc.h>
|
||||
#include <xrpl/protocol/RPCErr.h>
|
||||
#include <xrpl/protocol/SystemParameters.h>
|
||||
#include <xrpl/protocol/jss.h>
|
||||
@@ -67,6 +68,7 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -105,6 +107,38 @@ statusRequestResponse(HttpRequestType const& request, boost::beast::http::status
|
||||
return handoff;
|
||||
}
|
||||
|
||||
/**
|
||||
* The object a request presents its parameters in.
|
||||
*
|
||||
* `params` is an array holding one object for the by-position form and is that
|
||||
* object itself for the by-name form. Anything else reads as an empty object,
|
||||
* since a caller reads the version and the credentials out of it before the
|
||||
* shape is validated.
|
||||
*
|
||||
* Answers an Object or a Null and nothing else: Role.cpp reads `admin_password`
|
||||
* out of the result through the const `operator[]`, which asserts on any other
|
||||
* type, so widening this makes that line a remotely reachable assert.
|
||||
*
|
||||
* @param request The request, which need not be an object.
|
||||
* @return The parameters, or an empty object when the request presents none.
|
||||
* Borrowed from @p request, so it lives as long as the request does.
|
||||
*/
|
||||
static json::Value const&
|
||||
requestParams(json::Value const& request)
|
||||
{
|
||||
static json::Value const kNoParams{json::ValueType::Object};
|
||||
if (!request.isObject())
|
||||
return kNoParams;
|
||||
|
||||
auto const& params = request[jss::params];
|
||||
if (params.isArray() && params.size() > 0 && params[json::UInt(0)].isObjectOrNull())
|
||||
return params[json::UInt(0)];
|
||||
if (params.isObject())
|
||||
return params;
|
||||
|
||||
return kNoParams;
|
||||
}
|
||||
|
||||
// VFALCO TODO Rewrite to use boost::beast::http::fields
|
||||
static bool
|
||||
authorized(Port const& port, std::map<std::string, std::string> const& h)
|
||||
@@ -343,6 +377,16 @@ ServerHandler::onWSMessage(
|
||||
auto const size = boost::asio::buffer_size(buffers);
|
||||
if (size > rpc::tuning::kMaxRequestSize || !json::Reader{}.parse(jv, buffers) || !jv.isObject())
|
||||
{
|
||||
// The only place that can account for an unreadable frame: it never reaches
|
||||
// processSession, where every other request is charged.
|
||||
auto const is = std::static_pointer_cast<WSInfoSub>(session->appDefined);
|
||||
is->getConsumer().charge(resource::kFeeMalformedRpc);
|
||||
if (is->getConsumer().disconnect(journal_))
|
||||
{
|
||||
session->close({boost::beast::websocket::policy_error, "threshold exceeded"});
|
||||
return;
|
||||
}
|
||||
|
||||
// An unparsed body cannot be masked field-wise, so its size goes instead of its content.
|
||||
// Clamped rather than narrowed: json has no integer wider than 32 bits.
|
||||
json::Value jvResult(json::ValueType::Object);
|
||||
@@ -597,10 +641,40 @@ makeJsonError(json::Int code, json::Value&& message)
|
||||
return r;
|
||||
}
|
||||
|
||||
constexpr json::Int kMethodNotFound = -32601;
|
||||
constexpr json::Int kServerOverloaded = -32604;
|
||||
constexpr json::Int kForbidden = -32605;
|
||||
constexpr json::Int kWrongVersion = -32606;
|
||||
// Version of the JSON-RPC reply envelope, selected by the `ripplerpc` request parameter. It shapes
|
||||
// a completed result into a reply; no handler observes it.
|
||||
enum class RpcVersion {
|
||||
// Errors are returned under `result`, keyed by `error_message`, and echo the (secret-masked)
|
||||
// request. HTTP status is always 200.
|
||||
V1,
|
||||
// Errors are returned under `error`, keyed by `message`, and the request is not echoed. HTTP
|
||||
// status is always 200.
|
||||
V2,
|
||||
// As V2, but the error code selects a 4xx/5xx HTTP status.
|
||||
V3,
|
||||
};
|
||||
|
||||
constexpr RpcVersion kRpcVersionIfUnspecified = RpcVersion::V1;
|
||||
|
||||
/**
|
||||
* Maps a `ripplerpc` value onto the envelope version it selects.
|
||||
*
|
||||
* The value is unauthenticated, so anything but an exact match is refused.
|
||||
*
|
||||
* @param value The `ripplerpc` member, as sent.
|
||||
* @return The envelope version, or nullopt for the caller to reject.
|
||||
*/
|
||||
static std::optional<RpcVersion>
|
||||
rpcVersion(std::string_view value)
|
||||
{
|
||||
if (value == rpc::kRippleRpcVersion1)
|
||||
return RpcVersion::V1;
|
||||
if (value == rpc::kRippleRpcVersion2)
|
||||
return RpcVersion::V2;
|
||||
if (value == rpc::kRippleRpcVersion3)
|
||||
return RpcVersion::V3;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/**
|
||||
* The HTTP status the `ripplerpc: "3.0"` envelope reports for @p code.
|
||||
@@ -627,6 +701,84 @@ legacyHttpStatus(ErrorCodeI code)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shapes a handler @p result into the reply envelope for @p version, appending
|
||||
* it to @p reply.
|
||||
*
|
||||
* @param version The envelope the reply takes.
|
||||
* @param result A handler result, consumed.
|
||||
* @param request The request's parameters, echoed on a version 1 error only,
|
||||
* and where the `jsonrpc`, `ripplerpc` and `id` members are read.
|
||||
* @param batch Whether to append to @p reply rather than assign it.
|
||||
* @param reply The reply this writes.
|
||||
* @param journal Where an error is logged.
|
||||
* @return The HTTP status the reply is sent with. Only version 3 derives it
|
||||
* from the error code; the others report 200. A batch discards it.
|
||||
*/
|
||||
static int
|
||||
shapeReply(
|
||||
RpcVersion version,
|
||||
json::Value result,
|
||||
json::Value const& request,
|
||||
bool batch,
|
||||
json::Value& reply,
|
||||
beast::Journal journal)
|
||||
{
|
||||
json::Value r(json::ValueType::Object);
|
||||
int status = 200;
|
||||
|
||||
if (!result.isMember(jss::error))
|
||||
{
|
||||
result[jss::status] = jss::success;
|
||||
r[jss::result] = std::move(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Read through a const reference: the non-const `operator[]` inserts a null member for an
|
||||
// absent name, and the reply below is built out of `result`.
|
||||
json::Value const& reported = result;
|
||||
|
||||
JLOG(journal.debug()) << "rpcError: " << reported[jss::error] << ": "
|
||||
<< reported[jss::error_message];
|
||||
|
||||
if (version == RpcVersion::V3 && reported[jss::error_code].isInt())
|
||||
status = legacyHttpStatus(static_cast<ErrorCodeI>(reported[jss::error_code].asInt()));
|
||||
|
||||
result[jss::status] = jss::error;
|
||||
|
||||
if (version == RpcVersion::V1)
|
||||
{
|
||||
result[jss::request] = rpc::maskSecrets(request);
|
||||
r[jss::result] = std::move(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[jss::code] = result[jss::error_code];
|
||||
result[jss::message] = result[jss::error_message];
|
||||
result.removeMember(jss::error_message);
|
||||
r[jss::error] = std::move(result);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.isMember(jss::jsonrpc))
|
||||
r[jss::jsonrpc] = request[jss::jsonrpc];
|
||||
if (request.isMember(jss::ripplerpc))
|
||||
r[jss::ripplerpc] = request[jss::ripplerpc];
|
||||
if (request.isMember(jss::id))
|
||||
r[jss::id] = request[jss::id];
|
||||
|
||||
if (batch)
|
||||
{
|
||||
reply.append(std::move(r));
|
||||
}
|
||||
else
|
||||
{
|
||||
reply = std::move(r);
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void
|
||||
ServerHandler::processRequest(
|
||||
Port const& port,
|
||||
@@ -639,12 +791,55 @@ ServerHandler::processRequest(
|
||||
{
|
||||
auto const rpcJ = app_.getJournal("RPC");
|
||||
|
||||
// What a request presents when the server cannot read what it presents: an unparsable body or a
|
||||
// malformed entry carries no credentials to act on. A connection privileged by its address is
|
||||
// still privileged, `requestRole` reading the port rather than the request for that.
|
||||
static json::Value const kUnreadable(json::ValueType::Object);
|
||||
|
||||
// The resource entry a request is charged against before its own role is read. `Role::GUEST` is
|
||||
// the requirement asked for, no method having been read that could ask for more.
|
||||
auto const usageFor = [&](json::Value const& presented) {
|
||||
return requestInboundEndpoint(
|
||||
resourceManager_,
|
||||
remoteIPAddress,
|
||||
requestRole(Role::GUEST, port, presented, remoteIPAddress, user),
|
||||
user,
|
||||
forwardedFor);
|
||||
};
|
||||
|
||||
// Whether a charge also asks if the connection is over the drop threshold.
|
||||
enum class Threshold { Ignore, Ask };
|
||||
|
||||
// Charges a request rejected before its own role is read, and reports whether the connection is
|
||||
// over the drop threshold. Only a caller that can act on the answer passes `Threshold::Ask`,
|
||||
// since asking is itself a charge: see chargeUnreadBody below.
|
||||
auto const chargeWithoutRole = [&](json::Value const& presented, Threshold threshold) {
|
||||
auto usage = usageFor(presented);
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
return threshold == Threshold::Ask && usage.disconnect(journal_);
|
||||
};
|
||||
|
||||
// Charges a body the server rejects before it reads a request out of it. The drop threshold is
|
||||
// not asked about: `disconnect` is a mutator, charging the drop fee and counting a drop on
|
||||
// every call made while the balance is at or above the drop threshold, and this rejection
|
||||
// answers the whole body.
|
||||
auto const chargeUnreadBody = [&] { usageFor(kUnreadable).charge(resource::kFeeMalformedRpc); };
|
||||
|
||||
json::Value jsonOrig;
|
||||
{
|
||||
json::Reader reader;
|
||||
if ((request.size() > rpc::tuning::kMaxRequestSize) || !reader.parse(request, jsonOrig) ||
|
||||
!jsonOrig || !jsonOrig.isObject())
|
||||
// Only a parse failure can report the reader's reason, getFormattedErrorMessages being
|
||||
// built from what the reader recorded. The other three name their own cause.
|
||||
if (request.size() > rpc::tuning::kMaxRequestSize)
|
||||
{
|
||||
chargeUnreadBody();
|
||||
httpReply(400, "Request is too large", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
|
||||
json::Reader reader;
|
||||
if (!reader.parse(request, jsonOrig))
|
||||
{
|
||||
chargeUnreadBody();
|
||||
httpReply(
|
||||
400,
|
||||
"Unable to parse request: " + reader.getFormattedErrorMessages(),
|
||||
@@ -652,6 +847,23 @@ ServerHandler::processRequest(
|
||||
rpcJ);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jsonOrig)
|
||||
{
|
||||
// A well-formed document that carries nothing: `{}`, `[]` or `null`.
|
||||
chargeUnreadBody();
|
||||
httpReply(400, "Request is empty", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!jsonOrig.isObject())
|
||||
{
|
||||
// A non-empty array, the only value the reader accepts that is neither null nor an
|
||||
// object. A number, string or boolean is a parse failure, answered above.
|
||||
chargeUnreadBody();
|
||||
httpReply(400, "Request is not a JSON object", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool batch = false;
|
||||
@@ -663,6 +875,7 @@ ServerHandler::processRequest(
|
||||
batch = true;
|
||||
if (!jsonOrig.isMember(jss::params) || !jsonOrig[jss::params].isArray())
|
||||
{
|
||||
chargeUnreadBody();
|
||||
httpReply(400, "Malformed batch request", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
@@ -670,45 +883,83 @@ ServerHandler::processRequest(
|
||||
}
|
||||
|
||||
json::Value reply(batch ? json::ValueType::Array : json::ValueType::Object);
|
||||
// Only a lone request selects the HTTP status: a batch may mix versions and reports each
|
||||
// entry's outcome in its own reply, so the batch itself always succeeds.
|
||||
int httpStatus = 200;
|
||||
auto const start(std::chrono::high_resolution_clock::now());
|
||||
for (unsigned i = 0; i < size; ++i)
|
||||
{
|
||||
json::Value const& jsonRPC = batch ? jsonOrig[jss::params][i] : jsonOrig;
|
||||
|
||||
// Only an entry of a batch can be a non-object; a lone request was checked before the loop.
|
||||
// Inline rather than through `reject` below: an entry with no members carries the copy
|
||||
// under `request` whatever a caller asks for.
|
||||
if (!jsonRPC.isObject())
|
||||
{
|
||||
// Only a batch reaches here, so the loop can act on the threshold. The entry presents
|
||||
// no credentials, so the connection pays.
|
||||
bool const overloaded = chargeWithoutRole(kUnreadable, Threshold::Ask);
|
||||
|
||||
json::Value r(json::ValueType::Object);
|
||||
r[jss::request] = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "Method not found");
|
||||
r[jss::error] = makeJsonError(rpc::kJsonRpcMethodNotFound, "Method not found");
|
||||
reply.append(r);
|
||||
|
||||
if (overloaded)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
unsigned apiVersion = rpc::kApiVersionIfUnspecified;
|
||||
if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() &&
|
||||
jsonRPC[jss::params].size() > 0 && jsonRPC[jss::params][0u].isObject())
|
||||
{
|
||||
apiVersion = rpc::getAPIVersionNumber(
|
||||
jsonRPC[jss::params][json::UInt(0)], app_.config().betaRpcApi);
|
||||
}
|
||||
|
||||
// The version lives with the request's parameters, in whichever form the request presents
|
||||
// them. A `method: "batch"` entry that names none there carries it beside its method.
|
||||
unsigned apiVersion =
|
||||
rpc::getAPIVersionNumber(requestParams(jsonRPC), app_.config().betaRpcApi);
|
||||
if (apiVersion == rpc::kApiVersionIfUnspecified && batch)
|
||||
{
|
||||
// for batch request, api_version may be at a different level
|
||||
apiVersion = rpc::getAPIVersionNumber(jsonRPC, app_.config().betaRpcApi);
|
||||
}
|
||||
|
||||
// Answers a request rejected before it reaches a handler. A lone request receives the
|
||||
// HTTP status and `message` as the whole body; a batch entry receives an error object
|
||||
// beside a copy of the entry, one reply array having no other place to name the entry that
|
||||
// failed. `wrapRequest` puts that copy under `request` instead.
|
||||
//
|
||||
// Returns whether the loop continues, which it does only for a batch.
|
||||
auto const reject =
|
||||
[&](int status, json::Int code, char const* message, bool wrapRequest = false) {
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(status, message, output, rpcJ);
|
||||
return false;
|
||||
}
|
||||
json::Value r(json::ValueType::Object);
|
||||
if (!wrapRequest)
|
||||
{
|
||||
r = rpc::maskSecrets(jsonRPC);
|
||||
}
|
||||
else
|
||||
{
|
||||
r[jss::request] = rpc::maskSecrets(jsonRPC);
|
||||
}
|
||||
r[jss::error] = makeJsonError(code, message);
|
||||
reply.append(std::move(r));
|
||||
return true;
|
||||
};
|
||||
|
||||
if (apiVersion == rpc::kApiInvalidVersion)
|
||||
{
|
||||
if (!batch)
|
||||
bool const overloaded = chargeWithoutRole(
|
||||
requestParams(jsonRPC), batch ? Threshold::Ask : Threshold::Ignore);
|
||||
// An object-shaped rejection returns this entry under `request`, where a client
|
||||
// correlating by `reply[i].request` finds it.
|
||||
if (!reject(
|
||||
400,
|
||||
rpc::kJsonRpcWrongVersion,
|
||||
jss::invalid_API_version.cStr(),
|
||||
/*wrapRequest=*/true))
|
||||
{
|
||||
httpReply(400, jss::invalid_API_version.cStr(), output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r(json::ValueType::Object);
|
||||
r[jss::request] = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kWrongVersion, jss::invalid_API_version.cStr());
|
||||
reply.append(r);
|
||||
if (overloaded)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -721,65 +972,37 @@ ServerHandler::processRequest(
|
||||
apiVersion, app_.config().betaRpcApi, jsonRPC[jss::method].asString());
|
||||
}
|
||||
|
||||
if (jsonRPC.isMember(jss::params) && jsonRPC[jss::params].isArray() &&
|
||||
jsonRPC[jss::params].size() > 0 && jsonRPC[jss::params][json::UInt(0)].isObjectOrNull())
|
||||
{
|
||||
role = requestRole(
|
||||
required, port, jsonRPC[jss::params][json::UInt(0)], remoteIPAddress, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
role = requestRole(required, port, json::ValueType::Object, remoteIPAddress, user);
|
||||
}
|
||||
// The role comes from the credentials the request presents, which sit with its parameters
|
||||
// whichever form they take.
|
||||
role = requestRole(required, port, requestParams(jsonRPC), remoteIPAddress, user);
|
||||
|
||||
resource::Consumer usage;
|
||||
if (isUnlimited(role))
|
||||
auto usage =
|
||||
requestInboundEndpoint(resourceManager_, remoteIPAddress, role, user, forwardedFor);
|
||||
|
||||
// An overloaded server sheds the request without charging for it; disconnect() has already
|
||||
// accounted for the load that got it here. The loop then stops: every later entry of the
|
||||
// same body would be shed too, and answering each one is itself work an overloaded server
|
||||
// must not do.
|
||||
if (!isUnlimited(role) && usage.disconnect(journal_))
|
||||
{
|
||||
usage = resourceManager_.newUnlimitedEndpoint(remoteIPAddress);
|
||||
}
|
||||
else
|
||||
{
|
||||
usage = resourceManager_.newInboundEndpoint(
|
||||
remoteIPAddress, role == Role::PROXY, forwardedFor);
|
||||
if (usage.disconnect(journal_))
|
||||
{
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(503, "Server is overloaded", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kServerOverloaded, "Server is overloaded");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
if (!reject(503, rpc::kJsonRpcServerOverloaded, "Server is overloaded"))
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
if (role == Role::FORBID)
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(403, "Forbidden", output, rpcJ);
|
||||
if (!reject(403, rpc::kJsonRpcForbidden, "Forbidden"))
|
||||
return;
|
||||
}
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kForbidden, "Forbidden");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!jsonRPC.isMember(jss::method) || jsonRPC[jss::method].isNull())
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(400, "Null method", output, rpcJ);
|
||||
if (!reject(400, rpc::kJsonRpcMethodNotFound, "Null method"))
|
||||
return;
|
||||
}
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "Null method");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -787,14 +1010,8 @@ ServerHandler::processRequest(
|
||||
if (!method.isString())
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(400, "method is not string", output, rpcJ);
|
||||
if (!reject(400, rpc::kJsonRpcMethodNotFound, "method is not string"))
|
||||
return;
|
||||
}
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "method is not string");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -802,23 +1019,13 @@ ServerHandler::processRequest(
|
||||
if (strMethod.empty())
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(400, "method is empty", output, rpcJ);
|
||||
if (!reject(400, rpc::kJsonRpcMethodNotFound, "method is empty"))
|
||||
return;
|
||||
}
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "method is empty");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract request parameters from the request Json as `params`.
|
||||
//
|
||||
// If the field "params" is empty, `params` is an empty object.
|
||||
//
|
||||
// Otherwise, that field must be an array of length 1 (why?)
|
||||
// and we take that first entry and validate that it's an object.
|
||||
// The `params` field carries the one object a handler reads, in either of the two forms the
|
||||
// specification defines: the object itself, or an array holding it.
|
||||
json::Value params;
|
||||
if (!batch)
|
||||
{
|
||||
@@ -827,14 +1034,15 @@ ServerHandler::processRequest(
|
||||
{
|
||||
params = json::Value(json::ValueType::Object);
|
||||
}
|
||||
else if (!params.isArray() || params.size() != 1)
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
httpReply(400, "params unparsable", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
else
|
||||
else if (!params.isObject())
|
||||
{
|
||||
if (!params.isArray() || params.size() != 1)
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
httpReply(400, "params unparsable", output, rpcJ);
|
||||
return;
|
||||
}
|
||||
|
||||
params = std::move(params[0u]);
|
||||
if (!params.isObjectOrNull())
|
||||
{
|
||||
@@ -849,35 +1057,36 @@ ServerHandler::processRequest(
|
||||
params = jsonRPC;
|
||||
}
|
||||
|
||||
std::string ripplerpc{rpc::kRippleRpcVersion1};
|
||||
RpcVersion envelope = kRpcVersionIfUnspecified;
|
||||
if (params.isMember(jss::ripplerpc))
|
||||
{
|
||||
// A `ripplerpc` the server cannot honor is a bad parameter, so the second check reports
|
||||
// that code. The first keeps the method-not-found code shipped versions report.
|
||||
if (!params[jss::ripplerpc].isString())
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!batch)
|
||||
{
|
||||
httpReply(400, "ripplerpc is not a string", output, rpcJ);
|
||||
if (!reject(400, rpc::kJsonRpcMethodNotFound, "ripplerpc is not a string"))
|
||||
return;
|
||||
}
|
||||
|
||||
json::Value r = rpc::maskSecrets(jsonRPC);
|
||||
r[jss::error] = makeJsonError(kMethodNotFound, "ripplerpc is not a string");
|
||||
reply.append(r);
|
||||
continue;
|
||||
}
|
||||
ripplerpc = params[jss::ripplerpc].asString();
|
||||
|
||||
auto const parsed = rpcVersion(params[jss::ripplerpc].asString());
|
||||
if (!parsed)
|
||||
{
|
||||
usage.charge(resource::kFeeMalformedRpc);
|
||||
if (!reject(
|
||||
400, rpc::kJsonRpcInvalidParams, "ripplerpc is not a supported version"))
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
envelope = *parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear header-assigned values if not positively identified from a
|
||||
* secureGateway.
|
||||
*/
|
||||
if (role != Role::IDENTIFIED && role != Role::PROXY)
|
||||
{
|
||||
forwardedFor.remove_suffix(forwardedFor.size());
|
||||
user.remove_suffix(user.size());
|
||||
}
|
||||
// Header-assigned values belong to the connection, so an entry not identified from a
|
||||
// secureGateway drops them for itself only rather than clearing them in place.
|
||||
bool const identified = role == Role::IDENTIFIED || role == Role::PROXY;
|
||||
std::string_view const entryForwardedFor = identified ? forwardedFor : std::string_view{};
|
||||
std::string_view const entryUser = identified ? user : std::string_view{};
|
||||
|
||||
JLOG(journal_.debug()) << "Query: " << strMethod << rpc::loggable(params);
|
||||
|
||||
@@ -901,7 +1110,7 @@ ServerHandler::processRequest(
|
||||
.apiVersion = apiVersion,
|
||||
},
|
||||
params,
|
||||
{.user = user, .forwardedFor = forwardedFor},
|
||||
{.user = entryUser, .forwardedFor = entryForwardedFor},
|
||||
};
|
||||
json::Value result;
|
||||
|
||||
@@ -926,58 +1135,9 @@ ServerHandler::processRequest(
|
||||
if (usage.warn())
|
||||
result[jss::warning] = jss::load;
|
||||
|
||||
json::Value r(json::ValueType::Object);
|
||||
if (ripplerpc >= rpc::kRippleRpcVersion2)
|
||||
{
|
||||
if (result.isMember(jss::error))
|
||||
{
|
||||
result[jss::status] = jss::error;
|
||||
result["code"] = result[jss::error_code];
|
||||
result["message"] = result[jss::error_message];
|
||||
result.removeMember(jss::error_message);
|
||||
JLOG(journal_.debug())
|
||||
<< "rpcError: " << result[jss::error] << ": " << result[jss::error_message];
|
||||
r[jss::error] = std::move(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
result[jss::status] = jss::success;
|
||||
r[jss::result] = std::move(result);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Always report "status". On an error report the request as
|
||||
// received.
|
||||
if (result.isMember(jss::error))
|
||||
{
|
||||
result[jss::status] = jss::error;
|
||||
result[jss::request] = rpc::maskSecrets(params);
|
||||
|
||||
JLOG(journal_.debug())
|
||||
<< "rpcError: " << result[jss::error] << ": " << result[jss::error_message];
|
||||
}
|
||||
else
|
||||
{
|
||||
result[jss::status] = jss::success;
|
||||
}
|
||||
r[jss::result] = std::move(result);
|
||||
}
|
||||
|
||||
if (params.isMember(jss::jsonrpc))
|
||||
r[jss::jsonrpc] = params[jss::jsonrpc];
|
||||
if (params.isMember(jss::ripplerpc))
|
||||
r[jss::ripplerpc] = params[jss::ripplerpc];
|
||||
if (params.isMember(jss::id))
|
||||
r[jss::id] = params[jss::id];
|
||||
if (batch)
|
||||
{
|
||||
reply.append(std::move(r));
|
||||
}
|
||||
else
|
||||
{
|
||||
reply = std::move(r);
|
||||
}
|
||||
int const status = shapeReply(envelope, std::move(result), params, batch, reply, journal_);
|
||||
if (!batch)
|
||||
httpStatus = status;
|
||||
|
||||
if (reply.isMember(jss::result) && reply[jss::result].isMember(jss::result))
|
||||
{
|
||||
@@ -990,25 +1150,6 @@ ServerHandler::processRequest(
|
||||
}
|
||||
}
|
||||
|
||||
// If we're returning an error_code, use that to determine the HTTP status.
|
||||
int const httpStatus = [&reply] {
|
||||
// This feature is enabled with ripplerpc version 3.0 and above.
|
||||
// Before ripplerpc version 3.0 always return 200.
|
||||
if (reply.isMember(jss::ripplerpc) && reply[jss::ripplerpc].isString() &&
|
||||
reply[jss::ripplerpc].asString() >= rpc::kRippleRpcVersion3)
|
||||
{
|
||||
// If there's an error_code, use that to determine the HTTP Status.
|
||||
if (reply.isMember(jss::error) && reply[jss::error].isMember(jss::error_code) &&
|
||||
reply[jss::error][jss::error_code].isInt())
|
||||
{
|
||||
int const errCode = reply[jss::error][jss::error_code].asInt();
|
||||
return legacyHttpStatus(static_cast<ErrorCodeI>(errCode));
|
||||
}
|
||||
}
|
||||
// Return OK.
|
||||
return 200;
|
||||
}();
|
||||
|
||||
auto response = to_string(reply);
|
||||
|
||||
rpcTime_.notify(
|
||||
|
||||
Reference in new issue
Block a user