mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-19 18:45:52 +00:00
1715 lines
51 KiB
C++
1715 lines
51 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
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 <xrpld/rpc/RPCCall.h>
|
|
#include <xrpld/rpc/ServerHandler.h>
|
|
|
|
#include <xrpl/basics/ByteUtilities.h>
|
|
#include <xrpl/basics/Log.h>
|
|
#include <xrpl/basics/StringUtilities.h>
|
|
#include <xrpl/basics/base64.h>
|
|
#include <xrpl/basics/contract.h>
|
|
#include <xrpl/beast/core/LexicalCast.h>
|
|
#include <xrpl/json/json_forwards.h>
|
|
#include <xrpl/json/json_reader.h>
|
|
#include <xrpl/json/to_string.h>
|
|
#include <xrpl/net/HTTPClient.h>
|
|
#include <xrpl/protocol/ApiVersion.h>
|
|
#include <xrpl/protocol/ErrorCodes.h>
|
|
#include <xrpl/protocol/PublicKey.h>
|
|
#include <xrpl/protocol/RPCErr.h>
|
|
#include <xrpl/protocol/SystemParameters.h>
|
|
#include <xrpl/protocol/UintTypes.h>
|
|
#include <xrpl/protocol/jss.h>
|
|
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
#include <boost/asio/streambuf.hpp>
|
|
#include <boost/regex.hpp>
|
|
|
|
#include <array>
|
|
#include <iostream>
|
|
#include <type_traits>
|
|
#include <unordered_map>
|
|
|
|
namespace ripple {
|
|
|
|
class RPCParser;
|
|
|
|
//
|
|
// HTTP protocol
|
|
//
|
|
// This ain't Apache. We're just using HTTP header for the length field
|
|
// and to be compatible with other JSON-RPC implementations.
|
|
//
|
|
|
|
std::string
|
|
createHTTPPost(
|
|
std::string const& strHost,
|
|
std::string const& strPath,
|
|
std::string const& strMsg,
|
|
std::unordered_map<std::string, std::string> const& mapRequestHeaders)
|
|
{
|
|
std::ostringstream s;
|
|
|
|
// CHECKME this uses a different version than the replies below use. Is
|
|
// this by design or an accident or should it be using
|
|
// BuildInfo::getFullVersionString () as well?
|
|
|
|
s << "POST " << (strPath.empty() ? "/" : strPath) << " HTTP/1.0\r\n"
|
|
<< "User-Agent: " << systemName() << "-json-rpc/v1\r\n"
|
|
<< "Host: " << strHost << "\r\n"
|
|
<< "Content-Type: application/json\r\n"
|
|
<< "Content-Length: " << strMsg.size() << "\r\n"
|
|
<< "Accept: application/json\r\n";
|
|
|
|
for (auto const& [k, v] : mapRequestHeaders)
|
|
s << k << ": " << v << "\r\n";
|
|
|
|
s << "\r\n" << strMsg;
|
|
|
|
return s.str();
|
|
}
|
|
|
|
class RPCParser
|
|
{
|
|
private:
|
|
unsigned const apiVersion_;
|
|
beast::Journal const j_;
|
|
|
|
// TODO New routine for parsing ledger parameters, other routines should
|
|
// standardize on this.
|
|
static bool
|
|
jvParseLedger(Json::Value& jvRequest, std::string const& strLedger)
|
|
{
|
|
if (strLedger == "current" || strLedger == "closed" ||
|
|
strLedger == "validated")
|
|
{
|
|
jvRequest[jss::ledger_index] = strLedger;
|
|
}
|
|
else if (strLedger.length() == 64)
|
|
{
|
|
// YYY Could confirm this is a uint256.
|
|
jvRequest[jss::ledger_hash] = strLedger;
|
|
}
|
|
else
|
|
{
|
|
jvRequest[jss::ledger_index] =
|
|
beast::lexicalCast<std::uint32_t>(strLedger);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Build a object { "currency" : "XYZ", "issuer" : "rXYX" }
|
|
static Json::Value
|
|
jvParseCurrencyIssuer(std::string const& strCurrencyIssuer)
|
|
{
|
|
// Matches a sequence of 3 characters from
|
|
// `ripple::detail::isoCharSet` (the currency),
|
|
// optionally followed by a forward slash and some other characters
|
|
// (the issuer).
|
|
// https://www.boost.org/doc/libs/1_82_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html
|
|
static boost::regex reCurIss(
|
|
"\\`([][:alnum:]<>(){}[|?!@#$%^&*]{3})(?:/(.+))?\\'");
|
|
|
|
boost::smatch smMatch;
|
|
|
|
if (boost::regex_match(strCurrencyIssuer, smMatch, reCurIss))
|
|
{
|
|
Json::Value jvResult(Json::objectValue);
|
|
std::string strCurrency = smMatch[1];
|
|
std::string strIssuer = smMatch[2];
|
|
|
|
jvResult[jss::currency] = strCurrency;
|
|
|
|
if (strIssuer.length())
|
|
{
|
|
// Could confirm issuer is a valid Ripple address.
|
|
jvResult[jss::issuer] = strIssuer;
|
|
}
|
|
|
|
return jvResult;
|
|
}
|
|
else
|
|
{
|
|
return RPC::make_param_error(
|
|
std::string("Invalid currency/issuer '") + strCurrencyIssuer +
|
|
"'");
|
|
}
|
|
}
|
|
|
|
static bool
|
|
validPublicKey(
|
|
std::string const& strPk,
|
|
TokenType type = TokenType::AccountPublic)
|
|
{
|
|
if (parseBase58<ripple::PublicKey>(type, strPk))
|
|
return true;
|
|
|
|
auto pkHex = strUnHex(strPk);
|
|
if (!pkHex)
|
|
return false;
|
|
|
|
if (!publicKeyType(makeSlice(*pkHex)))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
using parseFuncPtr =
|
|
Json::Value (RPCParser::*)(Json::Value const& jvParams);
|
|
|
|
Json::Value
|
|
parseAsIs(Json::Value const& jvParams)
|
|
{
|
|
Json::Value v(Json::objectValue);
|
|
|
|
if (jvParams.isArray() && (jvParams.size() > 0))
|
|
v[jss::params] = jvParams;
|
|
|
|
return v;
|
|
}
|
|
|
|
Json::Value
|
|
parseInternal(Json::Value const& jvParams)
|
|
{
|
|
Json::Value v(Json::objectValue);
|
|
v[jss::internal_command] = jvParams[0u];
|
|
|
|
Json::Value params(Json::arrayValue);
|
|
|
|
for (unsigned i = 1; i < jvParams.size(); ++i)
|
|
params.append(jvParams[i]);
|
|
|
|
v[jss::params] = params;
|
|
|
|
return v;
|
|
}
|
|
|
|
Json::Value
|
|
parseManifest(Json::Value const& jvParams)
|
|
{
|
|
if (jvParams.size() == 1)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
std::string const strPk = jvParams[0u].asString();
|
|
if (!validPublicKey(strPk, TokenType::NodePublic))
|
|
return rpcError(rpcPUBLIC_MALFORMED);
|
|
|
|
jvRequest[jss::public_key] = strPk;
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
}
|
|
|
|
// fetch_info [clear]
|
|
Json::Value
|
|
parseFetchInfo(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
unsigned int iParams = jvParams.size();
|
|
|
|
if (iParams != 0)
|
|
jvRequest[jvParams[0u].asString()] = true;
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// account_tx accountID [ledger_min [ledger_max [limit [offset]]]] [binary]
|
|
// [count] [descending]
|
|
Json::Value
|
|
parseAccountTransactions(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
unsigned int iParams = jvParams.size();
|
|
|
|
auto const account = parseBase58<AccountID>(jvParams[0u].asString());
|
|
if (!account)
|
|
return rpcError(rpcACT_MALFORMED);
|
|
|
|
jvRequest[jss::account] = toBase58(*account);
|
|
|
|
bool bDone = false;
|
|
|
|
while (!bDone && iParams >= 2)
|
|
{
|
|
// VFALCO Why is Json::StaticString appearing on the right side?
|
|
if (jvParams[iParams - 1].asString() == jss::binary)
|
|
{
|
|
jvRequest[jss::binary] = true;
|
|
--iParams;
|
|
}
|
|
else if (jvParams[iParams - 1].asString() == jss::count)
|
|
{
|
|
jvRequest[jss::count] = true;
|
|
--iParams;
|
|
}
|
|
else if (jvParams[iParams - 1].asString() == jss::descending)
|
|
{
|
|
jvRequest[jss::descending] = true;
|
|
--iParams;
|
|
}
|
|
else
|
|
{
|
|
bDone = true;
|
|
}
|
|
}
|
|
|
|
if (1 == iParams)
|
|
{
|
|
}
|
|
else if (2 == iParams)
|
|
{
|
|
if (!jvParseLedger(jvRequest, jvParams[1u].asString()))
|
|
return jvRequest;
|
|
}
|
|
else
|
|
{
|
|
std::int64_t uLedgerMin = jvParams[1u].asInt();
|
|
std::int64_t uLedgerMax = jvParams[2u].asInt();
|
|
|
|
if (uLedgerMax != -1 && uLedgerMax < uLedgerMin)
|
|
{
|
|
if (apiVersion_ == 1)
|
|
return rpcError(rpcLGR_IDXS_INVALID);
|
|
return rpcError(rpcNOT_SYNCED);
|
|
}
|
|
|
|
jvRequest[jss::ledger_index_min] = jvParams[1u].asInt();
|
|
jvRequest[jss::ledger_index_max] = jvParams[2u].asInt();
|
|
|
|
if (iParams >= 4)
|
|
jvRequest[jss::limit] = jvParams[3u].asInt();
|
|
|
|
if (iParams >= 5)
|
|
jvRequest[jss::offset] = jvParams[4u].asInt();
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// book_offers <taker_pays> <taker_gets> [<taker> [<ledger> [<limit>
|
|
// [<proof> [<marker>]]]]] limit: 0 = no limit proof: 0 or 1
|
|
//
|
|
// Mnemonic: taker pays --> offer --> taker gets
|
|
Json::Value
|
|
parseBookOffers(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
Json::Value jvTakerPays =
|
|
jvParseCurrencyIssuer(jvParams[0u].asString());
|
|
Json::Value jvTakerGets =
|
|
jvParseCurrencyIssuer(jvParams[1u].asString());
|
|
|
|
if (isRpcError(jvTakerPays))
|
|
{
|
|
return jvTakerPays;
|
|
}
|
|
else
|
|
{
|
|
jvRequest[jss::taker_pays] = jvTakerPays;
|
|
}
|
|
|
|
if (isRpcError(jvTakerGets))
|
|
{
|
|
return jvTakerGets;
|
|
}
|
|
else
|
|
{
|
|
jvRequest[jss::taker_gets] = jvTakerGets;
|
|
}
|
|
|
|
if (jvParams.size() >= 3)
|
|
{
|
|
jvRequest[jss::issuer] = jvParams[2u].asString();
|
|
}
|
|
|
|
if (jvParams.size() >= 4 &&
|
|
!jvParseLedger(jvRequest, jvParams[3u].asString()))
|
|
return jvRequest;
|
|
|
|
if (jvParams.size() >= 5)
|
|
{
|
|
int iLimit = jvParams[5u].asInt();
|
|
|
|
if (iLimit > 0)
|
|
jvRequest[jss::limit] = iLimit;
|
|
}
|
|
|
|
if (jvParams.size() >= 6 && jvParams[5u].asInt())
|
|
{
|
|
jvRequest[jss::proof] = true;
|
|
}
|
|
|
|
if (jvParams.size() == 7)
|
|
jvRequest[jss::marker] = jvParams[6u];
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// can_delete [<ledgerid>|<ledgerhash>|now|always|never]
|
|
Json::Value
|
|
parseCanDelete(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
if (!jvParams.size())
|
|
return jvRequest;
|
|
|
|
std::string input = jvParams[0u].asString();
|
|
if (input.find_first_not_of("0123456789") == std::string::npos)
|
|
jvRequest["can_delete"] = jvParams[0u].asUInt();
|
|
else
|
|
jvRequest["can_delete"] = input;
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// connect <ip[:port]> [port]
|
|
Json::Value
|
|
parseConnect(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
std::string ip = jvParams[0u].asString();
|
|
if (jvParams.size() == 2)
|
|
{
|
|
jvRequest[jss::ip] = ip;
|
|
jvRequest[jss::port] = jvParams[1u].asUInt();
|
|
return jvRequest;
|
|
}
|
|
|
|
// handle case where there is one argument of the form ip:port
|
|
if (std::count(ip.begin(), ip.end(), ':') == 1)
|
|
{
|
|
std::size_t colon = ip.find_last_of(":");
|
|
jvRequest[jss::ip] = std::string{ip, 0, colon};
|
|
jvRequest[jss::port] =
|
|
Json::Value{std::string{ip, colon + 1}}.asUInt();
|
|
return jvRequest;
|
|
}
|
|
|
|
// default case, no port
|
|
jvRequest[jss::ip] = ip;
|
|
return jvRequest;
|
|
}
|
|
|
|
// deposit_authorized <source_account> <destination_account>
|
|
// [<ledger> [<credentials>, ...]]
|
|
Json::Value
|
|
parseDepositAuthorized(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
jvRequest[jss::source_account] = jvParams[0u].asString();
|
|
jvRequest[jss::destination_account] = jvParams[1u].asString();
|
|
|
|
if (jvParams.size() >= 3)
|
|
jvParseLedger(jvRequest, jvParams[2u].asString());
|
|
|
|
// 8 credentials max
|
|
if ((jvParams.size() >= 4) && (jvParams.size() <= 11))
|
|
{
|
|
jvRequest[jss::credentials] = Json::Value(Json::arrayValue);
|
|
for (uint32_t i = 3; i < jvParams.size(); ++i)
|
|
jvRequest[jss::credentials].append(jvParams[i].asString());
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// Return an error for attemping to subscribe/unsubscribe via RPC.
|
|
Json::Value
|
|
parseEvented(Json::Value const& jvParams)
|
|
{
|
|
return rpcError(rpcNO_EVENTS);
|
|
}
|
|
|
|
// feature [<feature>] [accept|reject]
|
|
Json::Value
|
|
parseFeature(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
if (jvParams.size() > 0)
|
|
jvRequest[jss::feature] = jvParams[0u].asString();
|
|
|
|
if (jvParams.size() > 1)
|
|
{
|
|
auto const action = jvParams[1u].asString();
|
|
|
|
// This may look reversed, but it's intentional: jss::vetoed
|
|
// determines whether an amendment is vetoed - so "reject" means
|
|
// that jss::vetoed is true.
|
|
if (boost::iequals(action, "reject"))
|
|
jvRequest[jss::vetoed] = Json::Value(true);
|
|
else if (boost::iequals(action, "accept"))
|
|
jvRequest[jss::vetoed] = Json::Value(false);
|
|
else
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// get_counts [<min_count>]
|
|
Json::Value
|
|
parseGetCounts(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
if (jvParams.size())
|
|
jvRequest[jss::min_count] = jvParams[0u].asUInt();
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// sign_for <account> <secret> <json> offline
|
|
// sign_for <account> <secret> <json>
|
|
Json::Value
|
|
parseSignFor(Json::Value const& jvParams)
|
|
{
|
|
bool const bOffline =
|
|
4 == jvParams.size() && jvParams[3u].asString() == "offline";
|
|
|
|
if (3 == jvParams.size() || bOffline)
|
|
{
|
|
Json::Value txJSON;
|
|
Json::Reader reader;
|
|
if (reader.parse(jvParams[2u].asString(), txJSON))
|
|
{
|
|
// sign_for txJSON.
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
jvRequest[jss::account] = jvParams[0u].asString();
|
|
jvRequest[jss::secret] = jvParams[1u].asString();
|
|
jvRequest[jss::tx_json] = txJSON;
|
|
|
|
if (bOffline)
|
|
jvRequest[jss::offline] = true;
|
|
|
|
return jvRequest;
|
|
}
|
|
}
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
}
|
|
|
|
// json <command> <json>
|
|
Json::Value
|
|
parseJson(Json::Value const& jvParams)
|
|
{
|
|
Json::Reader reader;
|
|
Json::Value jvRequest;
|
|
|
|
JLOG(j_.trace()) << "RPC method: " << jvParams[0u];
|
|
JLOG(j_.trace()) << "RPC json: " << jvParams[1u];
|
|
|
|
if (reader.parse(jvParams[1u].asString(), jvRequest))
|
|
{
|
|
if (!jvRequest.isObjectOrNull())
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
|
|
jvRequest[jss::method] = jvParams[0u];
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
}
|
|
|
|
bool
|
|
isValidJson2(Json::Value const& jv)
|
|
{
|
|
if (jv.isArray())
|
|
{
|
|
if (jv.size() == 0)
|
|
return false;
|
|
for (auto const& j : jv)
|
|
{
|
|
if (!isValidJson2(j))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
if (jv.isObject())
|
|
{
|
|
if (jv.isMember(jss::jsonrpc) && jv[jss::jsonrpc] == "2.0" &&
|
|
jv.isMember(jss::ripplerpc) && jv[jss::ripplerpc] == "2.0" &&
|
|
jv.isMember(jss::id) && jv.isMember(jss::method))
|
|
{
|
|
if (jv.isMember(jss::params) &&
|
|
!(jv[jss::params].isNull() || jv[jss::params].isArray() ||
|
|
jv[jss::params].isObject()))
|
|
return false;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Json::Value
|
|
parseJson2(Json::Value const& jvParams)
|
|
{
|
|
Json::Reader reader;
|
|
Json::Value jv;
|
|
bool valid_parse = reader.parse(jvParams[0u].asString(), jv);
|
|
if (valid_parse && isValidJson2(jv))
|
|
{
|
|
if (jv.isObject())
|
|
{
|
|
Json::Value jv1{Json::objectValue};
|
|
if (jv.isMember(jss::params))
|
|
{
|
|
auto const& params = jv[jss::params];
|
|
for (auto i = params.begin(); i != params.end(); ++i)
|
|
jv1[i.key().asString()] = *i;
|
|
}
|
|
jv1[jss::jsonrpc] = jv[jss::jsonrpc];
|
|
jv1[jss::ripplerpc] = jv[jss::ripplerpc];
|
|
jv1[jss::id] = jv[jss::id];
|
|
jv1[jss::method] = jv[jss::method];
|
|
return jv1;
|
|
}
|
|
// else jv.isArray()
|
|
Json::Value jv1{Json::arrayValue};
|
|
for (Json::UInt j = 0; j < jv.size(); ++j)
|
|
{
|
|
if (jv[j].isMember(jss::params))
|
|
{
|
|
auto const& params = jv[j][jss::params];
|
|
for (auto i = params.begin(); i != params.end(); ++i)
|
|
jv1[j][i.key().asString()] = *i;
|
|
}
|
|
jv1[j][jss::jsonrpc] = jv[j][jss::jsonrpc];
|
|
jv1[j][jss::ripplerpc] = jv[j][jss::ripplerpc];
|
|
jv1[j][jss::id] = jv[j][jss::id];
|
|
jv1[j][jss::method] = jv[j][jss::method];
|
|
}
|
|
return jv1;
|
|
}
|
|
auto jv_error = rpcError(rpcINVALID_PARAMS);
|
|
if (jv.isMember(jss::jsonrpc))
|
|
jv_error[jss::jsonrpc] = jv[jss::jsonrpc];
|
|
if (jv.isMember(jss::ripplerpc))
|
|
jv_error[jss::ripplerpc] = jv[jss::ripplerpc];
|
|
if (jv.isMember(jss::id))
|
|
jv_error[jss::id] = jv[jss::id];
|
|
return jv_error;
|
|
}
|
|
|
|
// ledger [id|index|current|closed|validated] [full|tx]
|
|
Json::Value
|
|
parseLedger(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
if (!jvParams.size())
|
|
{
|
|
return jvRequest;
|
|
}
|
|
|
|
jvParseLedger(jvRequest, jvParams[0u].asString());
|
|
|
|
if (2 == jvParams.size())
|
|
{
|
|
if (jvParams[1u].asString() == "full")
|
|
{
|
|
jvRequest[jss::full] = true;
|
|
}
|
|
else if (jvParams[1u].asString() == "tx")
|
|
{
|
|
jvRequest[jss::transactions] = true;
|
|
jvRequest[jss::expand] = true;
|
|
}
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// ledger_header <id>|<index>
|
|
Json::Value
|
|
parseLedgerId(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
std::string strLedger = jvParams[0u].asString();
|
|
|
|
if (strLedger.length() == 64)
|
|
{
|
|
jvRequest[jss::ledger_hash] = strLedger;
|
|
}
|
|
else
|
|
{
|
|
jvRequest[jss::ledger_index] =
|
|
beast::lexicalCast<std::uint32_t>(strLedger);
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// ledger_entry [id] [<index>]
|
|
Json::Value
|
|
parseLedgerEntry(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
jvRequest[jss::index] = jvParams[0u].asString();
|
|
|
|
if (jvParams.size() == 2 &&
|
|
!jvParseLedger(jvRequest, jvParams[1u].asString()))
|
|
return rpcError(rpcLGR_IDX_MALFORMED);
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// log_level: Get log levels
|
|
// log_level <severity>: Set master log level to the
|
|
// specified severity log_level <partition> <severity>: Set specified
|
|
// partition to specified severity
|
|
Json::Value
|
|
parseLogLevel(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
if (jvParams.size() == 1)
|
|
{
|
|
jvRequest[jss::severity] = jvParams[0u].asString();
|
|
}
|
|
else if (jvParams.size() == 2)
|
|
{
|
|
jvRequest[jss::partition] = jvParams[0u].asString();
|
|
jvRequest[jss::severity] = jvParams[1u].asString();
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// owner_info <account>
|
|
// account_info <account> [<ledger>]
|
|
// account_offers <account> [<ledger>]
|
|
Json::Value
|
|
parseAccountItems(Json::Value const& jvParams)
|
|
{
|
|
return parseAccountRaw1(jvParams);
|
|
}
|
|
|
|
Json::Value
|
|
parseAccountCurrencies(Json::Value const& jvParams)
|
|
{
|
|
return parseAccountRaw1(jvParams);
|
|
}
|
|
|
|
// account_lines <account> <account>|"" [<ledger>]
|
|
Json::Value
|
|
parseAccountLines(Json::Value const& jvParams)
|
|
{
|
|
return parseAccountRaw2(jvParams, jss::peer);
|
|
}
|
|
|
|
// account_channels <account> <account>|"" [<ledger>]
|
|
Json::Value
|
|
parseAccountChannels(Json::Value const& jvParams)
|
|
{
|
|
return parseAccountRaw2(jvParams, jss::destination_account);
|
|
}
|
|
|
|
// channel_authorize: <private_key> [<key_type>] <channel_id> <drops>
|
|
Json::Value
|
|
parseChannelAuthorize(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
unsigned int index = 0;
|
|
|
|
if (jvParams.size() == 4)
|
|
{
|
|
jvRequest[jss::passphrase] = jvParams[index];
|
|
index++;
|
|
|
|
if (!keyTypeFromString(jvParams[index].asString()))
|
|
return rpcError(rpcBAD_KEY_TYPE);
|
|
jvRequest[jss::key_type] = jvParams[index];
|
|
index++;
|
|
}
|
|
else
|
|
{
|
|
jvRequest[jss::secret] = jvParams[index];
|
|
index++;
|
|
}
|
|
|
|
{
|
|
// verify the channel id is a valid 256 bit number
|
|
uint256 channelId;
|
|
if (!channelId.parseHex(jvParams[index].asString()))
|
|
return rpcError(rpcCHANNEL_MALFORMED);
|
|
jvRequest[jss::channel_id] = to_string(channelId);
|
|
index++;
|
|
}
|
|
|
|
if (!jvParams[index].isString() ||
|
|
!to_uint64(jvParams[index].asString()))
|
|
return rpcError(rpcCHANNEL_AMT_MALFORMED);
|
|
jvRequest[jss::amount] = jvParams[index];
|
|
|
|
// If additional parameters are appended, be sure to increment index
|
|
// here
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// channel_verify <public_key> <channel_id> <drops> <signature>
|
|
Json::Value
|
|
parseChannelVerify(Json::Value const& jvParams)
|
|
{
|
|
std::string const strPk = jvParams[0u].asString();
|
|
|
|
if (!validPublicKey(strPk))
|
|
return rpcError(rpcPUBLIC_MALFORMED);
|
|
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
jvRequest[jss::public_key] = strPk;
|
|
{
|
|
// verify the channel id is a valid 256 bit number
|
|
uint256 channelId;
|
|
if (!channelId.parseHex(jvParams[1u].asString()))
|
|
return rpcError(rpcCHANNEL_MALFORMED);
|
|
}
|
|
jvRequest[jss::channel_id] = jvParams[1u].asString();
|
|
|
|
if (!jvParams[2u].isString() || !to_uint64(jvParams[2u].asString()))
|
|
return rpcError(rpcCHANNEL_AMT_MALFORMED);
|
|
jvRequest[jss::amount] = jvParams[2u];
|
|
|
|
jvRequest[jss::signature] = jvParams[3u].asString();
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
Json::Value
|
|
parseAccountRaw2(Json::Value const& jvParams, char const* const acc2Field)
|
|
{
|
|
std::array<char const* const, 2> accFields{{jss::account, acc2Field}};
|
|
auto const nParams = jvParams.size();
|
|
Json::Value jvRequest(Json::objectValue);
|
|
for (auto i = 0; i < nParams; ++i)
|
|
{
|
|
std::string strParam = jvParams[i].asString();
|
|
|
|
if (i == 1 && strParam.empty())
|
|
continue;
|
|
|
|
// Parameters 0 and 1 are accounts
|
|
if (i < 2)
|
|
{
|
|
if (parseBase58<AccountID>(strParam))
|
|
{
|
|
jvRequest[accFields[i]] = std::move(strParam);
|
|
}
|
|
else
|
|
{
|
|
return rpcError(rpcACT_MALFORMED);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (jvParseLedger(jvRequest, strParam))
|
|
return jvRequest;
|
|
return rpcError(rpcLGR_IDX_MALFORMED);
|
|
}
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// TODO: Get index from an alternate syntax: rXYZ:<index>
|
|
Json::Value
|
|
parseAccountRaw1(Json::Value const& jvParams)
|
|
{
|
|
std::string strIdent = jvParams[0u].asString();
|
|
unsigned int iCursor = jvParams.size();
|
|
|
|
if (!parseBase58<AccountID>(strIdent))
|
|
return rpcError(rpcACT_MALFORMED);
|
|
|
|
// Get info on account.
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
jvRequest[jss::account] = strIdent;
|
|
|
|
if (iCursor == 2 && !jvParseLedger(jvRequest, jvParams[1u].asString()))
|
|
return rpcError(rpcLGR_IDX_MALFORMED);
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
Json::Value
|
|
parseVault(Json::Value const& jvParams)
|
|
{
|
|
std::string strVaultID = jvParams[0u].asString();
|
|
uint256 id = beast::zero;
|
|
if (!id.parseHex(strVaultID))
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
|
|
Json::Value jvRequest(Json::objectValue);
|
|
jvRequest[jss::vault_id] = strVaultID;
|
|
|
|
if (jvParams.size() > 1)
|
|
jvParseLedger(jvRequest, jvParams[1u].asString());
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// peer_reservations_add <public_key> [<name>]
|
|
Json::Value
|
|
parsePeerReservationsAdd(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest;
|
|
jvRequest[jss::public_key] = jvParams[0u].asString();
|
|
if (jvParams.size() > 1)
|
|
{
|
|
jvRequest[jss::description] = jvParams[1u].asString();
|
|
}
|
|
return jvRequest;
|
|
}
|
|
|
|
// peer_reservations_del <public_key>
|
|
Json::Value
|
|
parsePeerReservationsDel(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest;
|
|
jvRequest[jss::public_key] = jvParams[0u].asString();
|
|
return jvRequest;
|
|
}
|
|
|
|
// ripple_path_find <json> [<ledger>]
|
|
Json::Value
|
|
parseRipplePathFind(Json::Value const& jvParams)
|
|
{
|
|
Json::Reader reader;
|
|
Json::Value jvRequest{Json::objectValue};
|
|
bool bLedger = 2 == jvParams.size();
|
|
|
|
JLOG(j_.trace()) << "RPC json: " << jvParams[0u];
|
|
|
|
if (reader.parse(jvParams[0u].asString(), jvRequest))
|
|
{
|
|
if (bLedger)
|
|
{
|
|
jvParseLedger(jvRequest, jvParams[1u].asString());
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
}
|
|
|
|
// simulate any transaction on the network
|
|
//
|
|
// simulate <tx_blob> [binary]
|
|
// simulate <tx_json> [binary]
|
|
Json::Value
|
|
parseSimulate(Json::Value const& jvParams)
|
|
{
|
|
Json::Value txJSON;
|
|
Json::Reader reader;
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
if (reader.parse(jvParams[0u].asString(), txJSON))
|
|
{
|
|
jvRequest[jss::tx_json] = txJSON;
|
|
}
|
|
else
|
|
{
|
|
jvRequest[jss::tx_blob] = jvParams[0u].asString();
|
|
}
|
|
|
|
if (jvParams.size() == 2)
|
|
{
|
|
if (!jvParams[1u].isString() || jvParams[1u].asString() != "binary")
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
jvRequest[jss::binary] = true;
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// sign/submit any transaction to the network
|
|
//
|
|
// sign <private_key> <json> offline
|
|
// submit <private_key> <json>
|
|
// submit <tx_blob>
|
|
Json::Value
|
|
parseSignSubmit(Json::Value const& jvParams)
|
|
{
|
|
Json::Value txJSON;
|
|
Json::Reader reader;
|
|
bool const bOffline =
|
|
3 == jvParams.size() && jvParams[2u].asString() == "offline";
|
|
|
|
if (1 == jvParams.size())
|
|
{
|
|
// Submitting tx_blob
|
|
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
jvRequest[jss::tx_blob] = jvParams[0u].asString();
|
|
|
|
return jvRequest;
|
|
}
|
|
else if (
|
|
(2 == jvParams.size() || bOffline) &&
|
|
reader.parse(jvParams[1u].asString(), txJSON))
|
|
{
|
|
// Signing or submitting tx_json.
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
jvRequest[jss::secret] = jvParams[0u].asString();
|
|
jvRequest[jss::tx_json] = txJSON;
|
|
|
|
if (bOffline)
|
|
jvRequest[jss::offline] = true;
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
}
|
|
|
|
// submit any multisigned transaction to the network
|
|
//
|
|
// submit_multisigned <json>
|
|
Json::Value
|
|
parseSubmitMultiSigned(Json::Value const& jvParams)
|
|
{
|
|
if (1 == jvParams.size())
|
|
{
|
|
Json::Value txJSON;
|
|
Json::Reader reader;
|
|
if (reader.parse(jvParams[0u].asString(), txJSON))
|
|
{
|
|
Json::Value jvRequest{Json::objectValue};
|
|
jvRequest[jss::tx_json] = txJSON;
|
|
return jvRequest;
|
|
}
|
|
}
|
|
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
}
|
|
|
|
// transaction_entry <tx_hash> <ledger_hash/ledger_index>
|
|
Json::Value
|
|
parseTransactionEntry(Json::Value const& jvParams)
|
|
{
|
|
// Parameter count should have already been verified.
|
|
XRPL_ASSERT(
|
|
jvParams.size() == 2,
|
|
"ripple::RPCParser::parseTransactionEntry : valid parameter count");
|
|
|
|
std::string const txHash = jvParams[0u].asString();
|
|
if (txHash.length() != 64)
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
|
|
Json::Value jvRequest{Json::objectValue};
|
|
jvRequest[jss::tx_hash] = txHash;
|
|
|
|
jvParseLedger(jvRequest, jvParams[1u].asString());
|
|
|
|
// jvParseLedger inserts a "ledger_index" of 0 if it doesn't
|
|
// find a match.
|
|
if (jvRequest.isMember(jss::ledger_index) &&
|
|
jvRequest[jss::ledger_index] == 0)
|
|
return rpcError(rpcINVALID_PARAMS);
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// tx <transaction_id>
|
|
Json::Value
|
|
parseTx(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
if (jvParams.size() == 2 || jvParams.size() == 4)
|
|
{
|
|
if (jvParams[1u].asString() == jss::binary)
|
|
jvRequest[jss::binary] = true;
|
|
}
|
|
|
|
if (jvParams.size() >= 3)
|
|
{
|
|
auto const offset = jvParams.size() == 3 ? 0 : 1;
|
|
|
|
jvRequest[jss::min_ledger] = jvParams[1u + offset].asString();
|
|
jvRequest[jss::max_ledger] = jvParams[2u + offset].asString();
|
|
}
|
|
|
|
if (jvParams[0u].asString().length() == 16)
|
|
jvRequest[jss::ctid] = jvParams[0u].asString();
|
|
else
|
|
jvRequest[jss::transaction] = jvParams[0u].asString();
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// tx_history <index>
|
|
Json::Value
|
|
parseTxHistory(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
jvRequest[jss::start] = jvParams[0u].asUInt();
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// validation_create [<pass_phrase>|<seed>|<seed_key>]
|
|
//
|
|
// NOTE: It is poor security to specify secret information on the command
|
|
// line. This information might be saved in the command shell history file
|
|
// (e.g. .bash_history) and it may be leaked via the process status command
|
|
// (i.e. ps).
|
|
Json::Value
|
|
parseValidationCreate(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
if (jvParams.size())
|
|
jvRequest[jss::secret] = jvParams[0u].asString();
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// wallet_propose [<passphrase>]
|
|
// <passphrase> is only for testing. Master seeds should only be generated
|
|
// randomly.
|
|
Json::Value
|
|
parseWalletPropose(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
if (jvParams.size())
|
|
jvRequest[jss::passphrase] = jvParams[0u].asString();
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// parse gateway balances
|
|
// gateway_balances [<ledger>] <issuer_account> [ <hotwallet> [ <hotwallet>
|
|
// ]]
|
|
|
|
Json::Value
|
|
parseGatewayBalances(Json::Value const& jvParams)
|
|
{
|
|
unsigned int index = 0;
|
|
unsigned int const size = jvParams.size();
|
|
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
std::string param = jvParams[index++].asString();
|
|
if (param.empty())
|
|
return RPC::make_param_error("Invalid first parameter");
|
|
|
|
if (param[0] != 'r')
|
|
{
|
|
if (param.size() == 64)
|
|
jvRequest[jss::ledger_hash] = param;
|
|
else
|
|
jvRequest[jss::ledger_index] = param;
|
|
|
|
if (size <= index)
|
|
return RPC::make_param_error("Invalid hotwallet");
|
|
|
|
param = jvParams[index++].asString();
|
|
}
|
|
|
|
jvRequest[jss::account] = param;
|
|
|
|
if (index < size)
|
|
{
|
|
Json::Value& hotWallets =
|
|
(jvRequest["hotwallet"] = Json::arrayValue);
|
|
while (index < size)
|
|
hotWallets.append(jvParams[index++].asString());
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// server_definitions [hash]
|
|
Json::Value
|
|
parseServerDefinitions(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest{Json::objectValue};
|
|
|
|
if (jvParams.size() == 1)
|
|
{
|
|
jvRequest[jss::hash] = jvParams[0u].asString();
|
|
}
|
|
|
|
return jvRequest;
|
|
}
|
|
|
|
// server_info [counters]
|
|
Json::Value
|
|
parseServerInfo(Json::Value const& jvParams)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
if (jvParams.size() == 1 && jvParams[0u].asString() == "counters")
|
|
jvRequest[jss::counters] = true;
|
|
return jvRequest;
|
|
}
|
|
|
|
public:
|
|
//--------------------------------------------------------------------------
|
|
|
|
explicit RPCParser(unsigned apiVersion, beast::Journal j)
|
|
: apiVersion_(apiVersion), j_(j)
|
|
{
|
|
}
|
|
|
|
//--------------------------------------------------------------------------
|
|
|
|
// Convert a rpc method and params to a request.
|
|
// <-- { method: xyz, params: [... ] } or { error: ..., ... }
|
|
Json::Value
|
|
parseCommand(
|
|
std::string strMethod,
|
|
Json::Value jvParams,
|
|
bool allowAnyCommand)
|
|
{
|
|
if (auto stream = j_.trace())
|
|
{
|
|
stream << "Method: '" << strMethod << "'";
|
|
stream << "Params: " << jvParams;
|
|
}
|
|
|
|
struct Command
|
|
{
|
|
char const* name;
|
|
parseFuncPtr parse;
|
|
int minParams;
|
|
int maxParams;
|
|
};
|
|
|
|
static constexpr Command commands[] = {
|
|
// Request-response methods
|
|
// - Returns an error, or the request.
|
|
// - To modify the method, provide a new method in the request.
|
|
{"account_currencies", &RPCParser::parseAccountCurrencies, 1, 3},
|
|
{"account_info", &RPCParser::parseAccountItems, 1, 3},
|
|
{"account_lines", &RPCParser::parseAccountLines, 1, 5},
|
|
{"account_channels", &RPCParser::parseAccountChannels, 1, 3},
|
|
{"account_nfts", &RPCParser::parseAccountItems, 1, 5},
|
|
{"account_objects", &RPCParser::parseAccountItems, 1, 5},
|
|
{"account_offers", &RPCParser::parseAccountItems, 1, 4},
|
|
{"account_tx", &RPCParser::parseAccountTransactions, 1, 8},
|
|
{"amm_info", &RPCParser::parseAsIs, 1, 2},
|
|
{"vault_info", &RPCParser::parseVault, 1, 2},
|
|
{"book_changes", &RPCParser::parseLedgerId, 1, 1},
|
|
{"book_offers", &RPCParser::parseBookOffers, 2, 7},
|
|
{"can_delete", &RPCParser::parseCanDelete, 0, 1},
|
|
{"channel_authorize", &RPCParser::parseChannelAuthorize, 3, 4},
|
|
{"channel_verify", &RPCParser::parseChannelVerify, 4, 4},
|
|
{"connect", &RPCParser::parseConnect, 1, 2},
|
|
{"consensus_info", &RPCParser::parseAsIs, 0, 0},
|
|
{"deposit_authorized", &RPCParser::parseDepositAuthorized, 2, 11},
|
|
{"feature", &RPCParser::parseFeature, 0, 2},
|
|
{"fetch_info", &RPCParser::parseFetchInfo, 0, 1},
|
|
{"gateway_balances", &RPCParser::parseGatewayBalances, 1, -1},
|
|
{"get_counts", &RPCParser::parseGetCounts, 0, 1},
|
|
{"json", &RPCParser::parseJson, 2, 2},
|
|
{"json2", &RPCParser::parseJson2, 1, 1},
|
|
{"ledger", &RPCParser::parseLedger, 0, 2},
|
|
{"ledger_accept", &RPCParser::parseAsIs, 0, 0},
|
|
{"ledger_closed", &RPCParser::parseAsIs, 0, 0},
|
|
{"ledger_current", &RPCParser::parseAsIs, 0, 0},
|
|
{"ledger_entry", &RPCParser::parseLedgerEntry, 1, 2},
|
|
{"ledger_header", &RPCParser::parseLedgerId, 1, 1},
|
|
{"ledger_request", &RPCParser::parseLedgerId, 1, 1},
|
|
{"log_level", &RPCParser::parseLogLevel, 0, 2},
|
|
{"logrotate", &RPCParser::parseAsIs, 0, 0},
|
|
{"manifest", &RPCParser::parseManifest, 1, 1},
|
|
{"owner_info", &RPCParser::parseAccountItems, 1, 3},
|
|
{"peers", &RPCParser::parseAsIs, 0, 0},
|
|
{"ping", &RPCParser::parseAsIs, 0, 0},
|
|
{"print", &RPCParser::parseAsIs, 0, 1},
|
|
// { "profile", &RPCParser::parseProfile, 1, 9
|
|
// },
|
|
{"random", &RPCParser::parseAsIs, 0, 0},
|
|
{"peer_reservations_add",
|
|
&RPCParser::parsePeerReservationsAdd,
|
|
1,
|
|
2},
|
|
{"peer_reservations_del",
|
|
&RPCParser::parsePeerReservationsDel,
|
|
1,
|
|
1},
|
|
{"peer_reservations_list", &RPCParser::parseAsIs, 0, 0},
|
|
{"ripple_path_find", &RPCParser::parseRipplePathFind, 1, 2},
|
|
{"server_definitions", &RPCParser::parseServerDefinitions, 0, 1},
|
|
{"server_info", &RPCParser::parseServerInfo, 0, 1},
|
|
{"server_state", &RPCParser::parseServerInfo, 0, 1},
|
|
{"sign", &RPCParser::parseSignSubmit, 2, 3},
|
|
{"sign_for", &RPCParser::parseSignFor, 3, 4},
|
|
{"stop", &RPCParser::parseAsIs, 0, 0},
|
|
{"simulate", &RPCParser::parseSimulate, 1, 2},
|
|
{"submit", &RPCParser::parseSignSubmit, 1, 3},
|
|
{"submit_multisigned", &RPCParser::parseSubmitMultiSigned, 1, 1},
|
|
{"transaction_entry", &RPCParser::parseTransactionEntry, 2, 2},
|
|
{"tx", &RPCParser::parseTx, 1, 4},
|
|
{"tx_history", &RPCParser::parseTxHistory, 1, 1},
|
|
{"unl_list", &RPCParser::parseAsIs, 0, 0},
|
|
{"validation_create", &RPCParser::parseValidationCreate, 0, 1},
|
|
{"validator_info", &RPCParser::parseAsIs, 0, 0},
|
|
{"version", &RPCParser::parseAsIs, 0, 0},
|
|
{"wallet_propose", &RPCParser::parseWalletPropose, 0, 1},
|
|
{"internal", &RPCParser::parseInternal, 1, -1},
|
|
|
|
// Evented methods
|
|
{"path_find", &RPCParser::parseEvented, -1, -1},
|
|
{"subscribe", &RPCParser::parseEvented, -1, -1},
|
|
{"unsubscribe", &RPCParser::parseEvented, -1, -1},
|
|
};
|
|
|
|
auto const count = jvParams.size();
|
|
|
|
for (auto const& command : commands)
|
|
{
|
|
if (strMethod == command.name)
|
|
{
|
|
if ((command.minParams >= 0 && count < command.minParams) ||
|
|
(command.maxParams >= 0 && count > command.maxParams))
|
|
{
|
|
JLOG(j_.debug())
|
|
<< "Wrong number of parameters for " << command.name
|
|
<< " minimum=" << command.minParams
|
|
<< " maximum=" << command.maxParams
|
|
<< " actual=" << count;
|
|
|
|
return rpcError(rpcBAD_SYNTAX);
|
|
}
|
|
|
|
return (this->*(command.parse))(jvParams);
|
|
}
|
|
}
|
|
|
|
// The command could not be found
|
|
if (!allowAnyCommand)
|
|
return rpcError(rpcUNKNOWN_COMMAND);
|
|
|
|
return parseAsIs(jvParams);
|
|
}
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
//
|
|
// JSON-RPC protocol. Bitcoin speaks version 1.0 for maximum compatibility,
|
|
// but uses JSON-RPC 1.1/2.0 standards for parts of the 1.0 standard that were
|
|
// unspecified (HTTP errors and contents of 'error').
|
|
//
|
|
// 1.0 spec: http://json-rpc.org/wiki/specification
|
|
// 1.2 spec: http://groups.google.com/group/json-rpc/web/json-rpc-over-http
|
|
//
|
|
|
|
std::string
|
|
JSONRPCRequest(
|
|
std::string const& strMethod,
|
|
Json::Value const& params,
|
|
Json::Value const& id)
|
|
{
|
|
Json::Value request;
|
|
request[jss::method] = strMethod;
|
|
request[jss::params] = params;
|
|
request[jss::id] = id;
|
|
return to_string(request) + "\n";
|
|
}
|
|
|
|
namespace {
|
|
// Special local exception type thrown when request can't be parsed.
|
|
class RequestNotParseable : public std::runtime_error
|
|
{
|
|
using std::runtime_error::runtime_error; // Inherit constructors
|
|
};
|
|
}; // namespace
|
|
|
|
struct RPCCallImp
|
|
{
|
|
explicit RPCCallImp() = default;
|
|
|
|
// VFALCO NOTE Is this a to-do comment or a doc comment?
|
|
// Place the async result somewhere useful.
|
|
static void
|
|
callRPCHandler(Json::Value* jvOutput, Json::Value const& jvInput)
|
|
{
|
|
(*jvOutput) = jvInput;
|
|
}
|
|
|
|
static bool
|
|
onResponse(
|
|
std::function<void(Json::Value const& jvInput)> callbackFuncP,
|
|
boost::system::error_code const& ecResult,
|
|
int iStatus,
|
|
std::string const& strData,
|
|
beast::Journal j)
|
|
{
|
|
if (callbackFuncP)
|
|
{
|
|
// Only care about the result, if we care to deliver it
|
|
// callbackFuncP.
|
|
|
|
// Receive reply
|
|
if (strData.empty())
|
|
Throw<std::runtime_error>(
|
|
"no response from server. Please "
|
|
"ensure that the rippled server is running in another "
|
|
"process.");
|
|
|
|
// Parse reply
|
|
JLOG(j.debug()) << "RPC reply: " << strData << std::endl;
|
|
if (strData.find("Unable to parse request") == 0 ||
|
|
strData.find(jss::invalid_API_version.c_str()) == 0)
|
|
Throw<RequestNotParseable>(strData);
|
|
Json::Reader reader;
|
|
Json::Value jvReply;
|
|
if (!reader.parse(strData, jvReply))
|
|
Throw<std::runtime_error>("couldn't parse reply from server");
|
|
|
|
if (!jvReply)
|
|
Throw<std::runtime_error>(
|
|
"expected reply to have result, error and id properties");
|
|
|
|
Json::Value jvResult(Json::objectValue);
|
|
|
|
jvResult["result"] = jvReply;
|
|
|
|
(callbackFuncP)(jvResult);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Build the request.
|
|
static void
|
|
onRequest(
|
|
std::string const& strMethod,
|
|
Json::Value const& jvParams,
|
|
std::unordered_map<std::string, std::string> const& headers,
|
|
std::string const& strPath,
|
|
boost::asio::streambuf& sb,
|
|
std::string const& strHost,
|
|
beast::Journal j)
|
|
{
|
|
JLOG(j.debug()) << "requestRPC: strPath='" << strPath << "'";
|
|
|
|
std::ostream osRequest(&sb);
|
|
osRequest << createHTTPPost(
|
|
strHost,
|
|
strPath,
|
|
JSONRPCRequest(strMethod, jvParams, Json::Value(1)),
|
|
headers);
|
|
}
|
|
};
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
// Used internally by rpcClient.
|
|
Json::Value
|
|
rpcCmdToJson(
|
|
std::vector<std::string> const& args,
|
|
Json::Value& retParams,
|
|
unsigned int apiVersion,
|
|
beast::Journal j)
|
|
{
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
RPCParser rpParser(apiVersion, j);
|
|
Json::Value jvRpcParams(Json::arrayValue);
|
|
|
|
for (int i = 1; i != args.size(); i++)
|
|
jvRpcParams.append(args[i]);
|
|
|
|
retParams = Json::Value(Json::objectValue);
|
|
|
|
retParams[jss::method] = args[0];
|
|
retParams[jss::params] = jvRpcParams;
|
|
|
|
jvRequest = rpParser.parseCommand(args[0], jvRpcParams, true);
|
|
|
|
auto insert_api_version = [apiVersion](Json::Value& jr) {
|
|
if (jr.isObject() && !jr.isMember(jss::error) &&
|
|
!jr.isMember(jss::api_version))
|
|
{
|
|
jr[jss::api_version] = apiVersion;
|
|
}
|
|
};
|
|
|
|
if (jvRequest.isObject())
|
|
insert_api_version(jvRequest);
|
|
else if (jvRequest.isArray())
|
|
std::for_each(jvRequest.begin(), jvRequest.end(), insert_api_version);
|
|
|
|
JLOG(j.trace()) << "RPC Request: " << jvRequest << std::endl;
|
|
return jvRequest;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
std::pair<int, Json::Value>
|
|
rpcClient(
|
|
std::vector<std::string> const& args,
|
|
Config const& config,
|
|
Logs& logs,
|
|
unsigned int apiVersion,
|
|
std::unordered_map<std::string, std::string> const& headers)
|
|
{
|
|
static_assert(
|
|
rpcBAD_SYNTAX == 1 && rpcSUCCESS == 0,
|
|
"Expect specific rpc enum values.");
|
|
if (args.empty())
|
|
return {rpcBAD_SYNTAX, {}}; // rpcBAD_SYNTAX = print usage
|
|
|
|
int nRet = rpcSUCCESS;
|
|
Json::Value jvOutput;
|
|
Json::Value jvRequest(Json::objectValue);
|
|
|
|
try
|
|
{
|
|
Json::Value jvRpc = Json::Value(Json::objectValue);
|
|
jvRequest =
|
|
rpcCmdToJson(args, jvRpc, apiVersion, logs.journal("RPCParser"));
|
|
|
|
if (jvRequest.isMember(jss::error))
|
|
{
|
|
jvOutput = jvRequest;
|
|
jvOutput["rpc"] = jvRpc;
|
|
}
|
|
else
|
|
{
|
|
ripple::ServerHandler::Setup setup;
|
|
try
|
|
{
|
|
setup = setup_ServerHandler(
|
|
config,
|
|
beast::logstream{logs.journal("HTTPClient").warn()});
|
|
}
|
|
catch (std::exception const&)
|
|
{
|
|
// ignore any exceptions, so the command
|
|
// line client works without a config file
|
|
}
|
|
|
|
if (config.rpc_ip)
|
|
{
|
|
setup.client.ip = config.rpc_ip->address().to_string();
|
|
setup.client.port = config.rpc_ip->port();
|
|
}
|
|
|
|
Json::Value jvParams(Json::arrayValue);
|
|
|
|
if (!setup.client.admin_user.empty())
|
|
jvRequest["admin_user"] = setup.client.admin_user;
|
|
|
|
if (!setup.client.admin_password.empty())
|
|
jvRequest["admin_password"] = setup.client.admin_password;
|
|
|
|
if (jvRequest.isObject())
|
|
jvParams.append(jvRequest);
|
|
else if (jvRequest.isArray())
|
|
{
|
|
for (Json::UInt i = 0; i < jvRequest.size(); ++i)
|
|
jvParams.append(jvRequest[i]);
|
|
}
|
|
|
|
{
|
|
boost::asio::io_service isService;
|
|
RPCCall::fromNetwork(
|
|
isService,
|
|
setup.client.ip,
|
|
setup.client.port,
|
|
setup.client.user,
|
|
setup.client.password,
|
|
"",
|
|
jvRequest.isMember(
|
|
jss::method) // Allow parser to rewrite method.
|
|
? jvRequest[jss::method].asString()
|
|
: jvRequest.isArray() ? "batch" : args[0],
|
|
jvParams, // Parsed, execute.
|
|
setup.client.secure != 0, // Use SSL
|
|
config.quiet(),
|
|
logs,
|
|
std::bind(
|
|
RPCCallImp::callRPCHandler,
|
|
&jvOutput,
|
|
std::placeholders::_1),
|
|
headers);
|
|
isService.run(); // This blocks until there are no more
|
|
// outstanding async calls.
|
|
}
|
|
if (jvOutput.isMember("result"))
|
|
{
|
|
// Had a successful JSON-RPC 2.0 call.
|
|
jvOutput = jvOutput["result"];
|
|
|
|
// jvOutput may report a server side error.
|
|
// It should report "status".
|
|
}
|
|
else
|
|
{
|
|
// Transport error.
|
|
Json::Value jvRpcError = jvOutput;
|
|
|
|
jvOutput = rpcError(rpcJSON_RPC);
|
|
jvOutput["result"] = jvRpcError;
|
|
}
|
|
|
|
// If had an error, supply invocation in result.
|
|
if (jvOutput.isMember(jss::error))
|
|
{
|
|
jvOutput["rpc"] =
|
|
jvRpc; // How the command was seen as method + params.
|
|
jvOutput["request_sent"] =
|
|
jvRequest; // How the command was translated.
|
|
}
|
|
}
|
|
|
|
if (jvOutput.isMember(jss::error))
|
|
{
|
|
jvOutput[jss::status] = "error";
|
|
if (jvOutput.isMember(jss::error_code))
|
|
nRet = std::stoi(jvOutput[jss::error_code].asString());
|
|
else if (jvOutput[jss::error].isMember(jss::error_code))
|
|
nRet =
|
|
std::stoi(jvOutput[jss::error][jss::error_code].asString());
|
|
else
|
|
nRet = rpcBAD_SYNTAX;
|
|
}
|
|
|
|
// YYY We could have a command line flag for single line output for
|
|
// scripts. YYY We would intercept output here and simplify it.
|
|
}
|
|
catch (RequestNotParseable& e)
|
|
{
|
|
jvOutput = rpcError(rpcINVALID_PARAMS);
|
|
jvOutput["error_what"] = e.what();
|
|
nRet = rpcINVALID_PARAMS;
|
|
}
|
|
catch (std::exception& e)
|
|
{
|
|
jvOutput = rpcError(rpcINTERNAL);
|
|
jvOutput["error_what"] = e.what();
|
|
nRet = rpcINTERNAL;
|
|
}
|
|
|
|
return {nRet, std::move(jvOutput)};
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
namespace RPCCall {
|
|
|
|
int
|
|
fromCommandLine(
|
|
Config const& config,
|
|
std::vector<std::string> const& vCmd,
|
|
Logs& logs)
|
|
{
|
|
auto const result =
|
|
rpcClient(vCmd, config, logs, RPC::apiCommandLineVersion);
|
|
|
|
std::cout << result.second.toStyledString();
|
|
|
|
return result.first;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------
|
|
|
|
void
|
|
fromNetwork(
|
|
boost::asio::io_service& io_service,
|
|
std::string const& strIp,
|
|
std::uint16_t const iPort,
|
|
std::string const& strUsername,
|
|
std::string const& strPassword,
|
|
std::string const& strPath,
|
|
std::string const& strMethod,
|
|
Json::Value const& jvParams,
|
|
bool const bSSL,
|
|
bool const quiet,
|
|
Logs& logs,
|
|
std::function<void(Json::Value const& jvInput)> callbackFuncP,
|
|
std::unordered_map<std::string, std::string> headers)
|
|
{
|
|
auto j = logs.journal("HTTPClient");
|
|
|
|
// Connect to localhost
|
|
if (!quiet)
|
|
{
|
|
JLOG(j.info()) << (bSSL ? "Securely connecting to " : "Connecting to ")
|
|
<< strIp << ":" << iPort << std::endl;
|
|
}
|
|
|
|
// HTTP basic authentication
|
|
headers["Authorization"] =
|
|
std::string("Basic ") + base64_encode(strUsername + ":" + strPassword);
|
|
|
|
// Send request
|
|
|
|
// Number of bytes to try to receive if no
|
|
// Content-Length header received
|
|
constexpr auto RPC_REPLY_MAX_BYTES = megabytes(256);
|
|
|
|
using namespace std::chrono_literals;
|
|
auto constexpr RPC_WEBHOOK_TIMEOUT = 30s;
|
|
|
|
HTTPClient::request(
|
|
bSSL,
|
|
io_service,
|
|
strIp,
|
|
iPort,
|
|
std::bind(
|
|
&RPCCallImp::onRequest,
|
|
strMethod,
|
|
jvParams,
|
|
headers,
|
|
strPath,
|
|
std::placeholders::_1,
|
|
std::placeholders::_2,
|
|
j),
|
|
RPC_REPLY_MAX_BYTES,
|
|
RPC_WEBHOOK_TIMEOUT,
|
|
std::bind(
|
|
&RPCCallImp::onResponse,
|
|
callbackFuncP,
|
|
std::placeholders::_1,
|
|
std::placeholders::_2,
|
|
std::placeholders::_3,
|
|
j),
|
|
j);
|
|
}
|
|
|
|
} // namespace RPCCall
|
|
|
|
} // namespace ripple
|