mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Update RPC handler role/usage (RIPD-557):
* Properly use the RPC method to determine required role for HTTP/S RPC calls. * Charge for malformed RPC calls over HTTP/S
This commit is contained in:
@@ -4620,6 +4620,10 @@
|
|||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="..\..\src\test\rpc\RPCOverload_test.cpp">
|
||||||
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||||
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="..\..\src\test\rpc\ServerInfo_test.cpp">
|
<ClCompile Include="..\..\src\test\rpc\ServerInfo_test.cpp">
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='debug|x64'">True</ExcludedFromBuild>
|
||||||
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
<ExcludedFromBuild Condition="'$(Configuration)|$(Platform)'=='release|x64'">True</ExcludedFromBuild>
|
||||||
|
|||||||
@@ -5262,6 +5262,9 @@
|
|||||||
<ClCompile Include="..\..\src\test\rpc\RobustTransaction_test.cpp">
|
<ClCompile Include="..\..\src\test\rpc\RobustTransaction_test.cpp">
|
||||||
<Filter>test\rpc</Filter>
|
<Filter>test\rpc</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="..\..\src\test\rpc\RPCOverload_test.cpp">
|
||||||
|
<Filter>test\rpc</Filter>
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="..\..\src\test\rpc\ServerInfo_test.cpp">
|
<ClCompile Include="..\..\src\test\rpc\ServerInfo_test.cpp">
|
||||||
<Filter>test\rpc</Filter>
|
<Filter>test\rpc</Filter>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
|||||||
@@ -402,6 +402,8 @@ ServerHandlerImp::processSession(
|
|||||||
if (is->getConsumer().disconnect())
|
if (is->getConsumer().disconnect())
|
||||||
{
|
{
|
||||||
session->close();
|
session->close();
|
||||||
|
// FIX: This rpcError is not delivered since the session
|
||||||
|
// was just closed.
|
||||||
return rpcError(rpcSLOW_DOWN);
|
return rpcError(rpcSLOW_DOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -438,6 +440,7 @@ ServerHandlerImp::processSession(
|
|||||||
is->user());
|
is->user());
|
||||||
if (Role::FORBID == role)
|
if (Role::FORBID == role)
|
||||||
{
|
{
|
||||||
|
loadType = Resource::feeInvalidRPC;
|
||||||
jr[jss::result] = rpcError (rpcFORBIDDEN);
|
jr[jss::result] = rpcError (rpcFORBIDDEN);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -550,26 +553,12 @@ ServerHandlerImp::processRequest (Port const& port,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse id now so errors from here on will have the id
|
/* ---------------------------------------------------------------------- */
|
||||||
//
|
// Determine role/usage so we can charge for invalid requests
|
||||||
// VFALCO NOTE Except that "id" isn't included in the following errors.
|
|
||||||
//
|
|
||||||
Json::Value const& id = jsonRPC [jss::id];
|
|
||||||
Json::Value const& method = jsonRPC [jss::method];
|
Json::Value const& method = jsonRPC [jss::method];
|
||||||
|
|
||||||
if (! method) {
|
|
||||||
HTTPReply (400, "Null method", output, rpcJ);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!method.isString ()) {
|
|
||||||
HTTPReply (400, "method is not string", output, rpcJ);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------- */
|
|
||||||
auto role = Role::FORBID;
|
auto role = Role::FORBID;
|
||||||
auto required = RPC::roleRequired(id.asString());
|
auto required = RPC::roleRequired(method.asString());
|
||||||
if (jsonRPC.isMember(jss::params) &&
|
if (jsonRPC.isMember(jss::params) &&
|
||||||
jsonRPC[jss::params].isArray() &&
|
jsonRPC[jss::params].isArray() &&
|
||||||
jsonRPC[jss::params].size() > 0 &&
|
jsonRPC[jss::params].size() > 0 &&
|
||||||
@@ -584,16 +573,6 @@ ServerHandlerImp::processRequest (Port const& port,
|
|||||||
remoteIPAddress, user);
|
remoteIPAddress, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear header-assigned values if not positively identified from a
|
|
||||||
* secure_gateway.
|
|
||||||
*/
|
|
||||||
if (role != Role::IDENTIFIED)
|
|
||||||
{
|
|
||||||
forwardedFor.clear();
|
|
||||||
user.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
Resource::Consumer usage;
|
Resource::Consumer usage;
|
||||||
if (isUnlimited(role))
|
if (isUnlimited(role))
|
||||||
{
|
{
|
||||||
@@ -610,9 +589,31 @@ ServerHandlerImp::processRequest (Port const& port,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (role == Role::FORBID)
|
||||||
|
{
|
||||||
|
usage.charge(Resource::feeInvalidRPC);
|
||||||
|
HTTPReply (403, "Forbidden", output, rpcJ);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! method)
|
||||||
|
{
|
||||||
|
usage.charge(Resource::feeInvalidRPC);
|
||||||
|
HTTPReply (400, "Null method", output, rpcJ);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! method.isString ())
|
||||||
|
{
|
||||||
|
usage.charge(Resource::feeInvalidRPC);
|
||||||
|
HTTPReply (400, "method is not string", output, rpcJ);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::string strMethod = method.asString ();
|
std::string strMethod = method.asString ();
|
||||||
if (strMethod.empty())
|
if (strMethod.empty())
|
||||||
{
|
{
|
||||||
|
usage.charge(Resource::feeInvalidRPC);
|
||||||
HTTPReply (400, "method is empty", output, rpcJ);
|
HTTPReply (400, "method is empty", output, rpcJ);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -630,6 +631,7 @@ ServerHandlerImp::processRequest (Port const& port,
|
|||||||
|
|
||||||
else if (!params.isArray () || params.size() != 1)
|
else if (!params.isArray () || params.size() != 1)
|
||||||
{
|
{
|
||||||
|
usage.charge(Resource::feeInvalidRPC);
|
||||||
HTTPReply (400, "params unparseable", output, rpcJ);
|
HTTPReply (400, "params unparseable", output, rpcJ);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -638,20 +640,20 @@ ServerHandlerImp::processRequest (Port const& port,
|
|||||||
params = std::move (params[0u]);
|
params = std::move (params[0u]);
|
||||||
if (!params.isObject())
|
if (!params.isObject())
|
||||||
{
|
{
|
||||||
|
usage.charge(Resource::feeInvalidRPC);
|
||||||
HTTPReply (400, "params unparseable", output, rpcJ);
|
HTTPReply (400, "params unparseable", output, rpcJ);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VFALCO TODO Shouldn't we handle this earlier?
|
/**
|
||||||
//
|
* Clear header-assigned values if not positively identified from a
|
||||||
if (role == Role::FORBID)
|
* secure_gateway.
|
||||||
|
*/
|
||||||
|
if (role != Role::IDENTIFIED)
|
||||||
{
|
{
|
||||||
// VFALCO TODO Needs implementing
|
forwardedFor.clear();
|
||||||
// FIXME Needs implementing
|
user.clear();
|
||||||
// XXX This needs rate limiting to prevent brute forcing password.
|
|
||||||
HTTPReply (403, "Forbidden", output, rpcJ);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
JLOG(m_journal.debug()) << "Query: " << strMethod << params;
|
JLOG(m_journal.debug()) << "Query: " << strMethod << params;
|
||||||
@@ -684,6 +686,10 @@ ServerHandlerImp::processRequest (Port const& port,
|
|||||||
result[jss::status] = jss::success;
|
result[jss::status] = jss::success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usage.charge (loadType);
|
||||||
|
if (usage.warn())
|
||||||
|
result[jss::warning] = jss::load;
|
||||||
|
|
||||||
Json::Value reply (Json::objectValue);
|
Json::Value reply (Json::objectValue);
|
||||||
reply[jss::result] = std::move (result);
|
reply[jss::result] = std::move (result);
|
||||||
if (jsonRPC.isMember(jss::jsonrpc))
|
if (jsonRPC.isMember(jss::jsonrpc))
|
||||||
@@ -702,7 +708,6 @@ ServerHandlerImp::processRequest (Port const& port,
|
|||||||
response.size ()));
|
response.size ()));
|
||||||
|
|
||||||
response += '\n';
|
response += '\n';
|
||||||
usage.charge (loadType);
|
|
||||||
|
|
||||||
if (auto stream = m_journal.debug())
|
if (auto stream = m_journal.debug())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ void HTTPReply (
|
|||||||
case 403: output ("HTTP/1.1 403 Forbidden\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 404: output ("HTTP/1.1 404 Not Found\r\n"); break;
|
||||||
case 500: output ("HTTP/1.1 500 Internal Server Error\r\n"); break;
|
case 500: output ("HTTP/1.1 500 Internal Server Error\r\n"); break;
|
||||||
|
case 503: output ("HTTP/1.1 503 Server is overloaded\r\n"); break;
|
||||||
}
|
}
|
||||||
|
|
||||||
output (getHTTPHeaderTimestamp ());
|
output (getHTTPHeaderTimestamp ());
|
||||||
|
|||||||
@@ -294,10 +294,12 @@ public:
|
|||||||
env.fund(XRP(100000), gw);
|
env.fund(XRP(100000), gw);
|
||||||
env.close();
|
env.close();
|
||||||
|
|
||||||
auto const result = env.rpc ( "ledger_request", "1" ) [jss::result];
|
auto const result = env.rpc ( "ledger_request", "1" ) [jss::result];
|
||||||
BEAST_EXPECT(result[jss::error] == "noPermission");
|
// The current HTTP/S ServerHandler returns an HTTP 403 error code here
|
||||||
BEAST_EXPECT(result[jss::status] == "error");
|
// rather than a noPermission JSON error. The JSONRPCClient just eats that
|
||||||
BEAST_EXPECT(result[jss::error_message] == "You don't have permission for this command.");
|
// error and returns an null result.
|
||||||
|
BEAST_EXPECT(result.type() == Json::nullValue);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void run ()
|
void run ()
|
||||||
|
|||||||
90
src/test/rpc/RPCOverload_test.cpp
Normal file
90
src/test/rpc/RPCOverload_test.cpp
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
/*
|
||||||
|
This file is part of rippled: https://github.com/ripple/rippled
|
||||||
|
Copyright (c) 2012, 2013 Ripple Labs Inc.
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
#include <BeastConfig.h>
|
||||||
|
#include <ripple/protocol/JsonFields.h>
|
||||||
|
#include <test/support/WSClient.h>
|
||||||
|
#include <test/support/JSONRPCClient.h>
|
||||||
|
#include <test/support/jtx.h>
|
||||||
|
#include <ripple/beast/unit_test.h>
|
||||||
|
|
||||||
|
namespace ripple {
|
||||||
|
namespace test {
|
||||||
|
|
||||||
|
class RPCOverload_test : public beast::unit_test::suite
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void testOverload(bool useWS)
|
||||||
|
{
|
||||||
|
testcase << "Overload " << (useWS ? "WS" : "HTTP") << " RPC client";
|
||||||
|
using namespace jtx;
|
||||||
|
Env env(*this, []()
|
||||||
|
{
|
||||||
|
auto p = std::make_unique<Config>();
|
||||||
|
setupConfigForUnitTests(*p);
|
||||||
|
(*p)["port_rpc"].set("admin","");
|
||||||
|
(*p)["port_ws"].set("admin","");
|
||||||
|
return p;
|
||||||
|
}());
|
||||||
|
|
||||||
|
Account const alice {"alice"};
|
||||||
|
Account const bob {"bob"};
|
||||||
|
env.fund (XRP (10000), alice, bob);
|
||||||
|
|
||||||
|
std::unique_ptr<AbstractClient> client = useWS ?
|
||||||
|
makeWSClient(env.app().config())
|
||||||
|
: makeJSONRPCClient(env.app().config());
|
||||||
|
|
||||||
|
Json::Value tx = Json::objectValue;
|
||||||
|
tx[jss::tx_json] = pay(alice, bob, XRP(1));
|
||||||
|
tx[jss::secret] = toBase58(generateSeed("alice"));
|
||||||
|
|
||||||
|
// Ask the server to repeatedly sign this transaction
|
||||||
|
// Signing is a resource heavy transaction, so we want the server
|
||||||
|
// to warn and eventually boot us.
|
||||||
|
bool warned = false, booted = false;
|
||||||
|
for(int i = 0 ; i < 500 && !booted; ++i)
|
||||||
|
{
|
||||||
|
auto jv = client->invoke("sign", tx);
|
||||||
|
if(!useWS)
|
||||||
|
jv = jv[jss::result];
|
||||||
|
// When booted, we just get a null json response
|
||||||
|
if(jv.isNull())
|
||||||
|
booted = true;
|
||||||
|
else
|
||||||
|
BEAST_EXPECT(jv.isMember(jss::status)
|
||||||
|
&& (jv[jss::status] == "success"));
|
||||||
|
|
||||||
|
if(jv.isMember(jss::warning))
|
||||||
|
warned = jv[jss::warning] == jss::load;
|
||||||
|
}
|
||||||
|
BEAST_EXPECT(warned && booted);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
void run() override
|
||||||
|
{
|
||||||
|
testOverload(false /* http */);
|
||||||
|
testOverload(true /* ws */);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
BEAST_DEFINE_TESTSUITE(RPCOverload,app,ripple);
|
||||||
|
|
||||||
|
} // test
|
||||||
|
} // ripple
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
#include <beast/core/placeholders.hpp>
|
#include <beast/core/placeholders.hpp>
|
||||||
#include <beast/core/streambuf.hpp>
|
#include <beast/core/streambuf.hpp>
|
||||||
#include <beast/websocket.hpp>
|
#include <beast/websocket.hpp>
|
||||||
|
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
|
||||||
#include <ripple/beast/unit_test.h>
|
#include <ripple/beast/unit_test.h>
|
||||||
@@ -97,6 +98,8 @@ class WSClientImpl : public WSClient
|
|||||||
beast::websocket::opcode op_;
|
beast::websocket::opcode op_;
|
||||||
beast::streambuf rb_;
|
beast::streambuf rb_;
|
||||||
|
|
||||||
|
bool peerClosed_ = false;
|
||||||
|
|
||||||
// synchronize destructor
|
// synchronize destructor
|
||||||
bool b0_ = false;
|
bool b0_ = false;
|
||||||
std::mutex m0_;
|
std::mutex m0_;
|
||||||
@@ -112,7 +115,8 @@ class WSClientImpl : public WSClient
|
|||||||
void cleanup()
|
void cleanup()
|
||||||
{
|
{
|
||||||
error_code ec;
|
error_code ec;
|
||||||
ws_.close({}, ec);
|
if(!peerClosed_)
|
||||||
|
ws_.close({}, ec);
|
||||||
stream_.close(ec);
|
stream_.close(ec);
|
||||||
work_ = boost::none;
|
work_ = boost::none;
|
||||||
thread_.join();
|
thread_.join();
|
||||||
@@ -255,7 +259,12 @@ private:
|
|||||||
on_read_msg(error_code const& ec)
|
on_read_msg(error_code const& ec)
|
||||||
{
|
{
|
||||||
if(ec)
|
if(ec)
|
||||||
|
{
|
||||||
|
if(ec == beast::websocket::error::closed)
|
||||||
|
peerClosed_ = true;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Json::Value jv;
|
Json::Value jv;
|
||||||
Json::Reader jr;
|
Json::Reader jr;
|
||||||
jr.parse(buffer_string(rb_.data()), jv);
|
jr.parse(buffer_string(rb_.data()), jv);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
#include <test/rpc/LedgerRequestRPC_test.cpp>
|
#include <test/rpc/LedgerRequestRPC_test.cpp>
|
||||||
#include <test/rpc/NoRipple_test.cpp>
|
#include <test/rpc/NoRipple_test.cpp>
|
||||||
#include <test/rpc/RobustTransaction_test.cpp>
|
#include <test/rpc/RobustTransaction_test.cpp>
|
||||||
|
#include <test/rpc/RPCOverload_test.cpp>
|
||||||
#include <test/rpc/ServerInfo_test.cpp>
|
#include <test/rpc/ServerInfo_test.cpp>
|
||||||
#include <test/rpc/Status_test.cpp>
|
#include <test/rpc/Status_test.cpp>
|
||||||
#include <test/rpc/Subscribe_test.cpp>
|
#include <test/rpc/Subscribe_test.cpp>
|
||||||
|
|||||||
Reference in New Issue
Block a user