Fulcrum/RPC.h
Calin Culianu a831cce8ac
Modified license preamble to say "Bitcoin Cash" instead of "Electron Cash"
The hope is maybe people will adopt this with other wallets...
2019-12-28 23:48:24 +02:00

381 lines
19 KiB
C++

//
// Fulcrum - A fast & nimble SPV Server for Bitcoin Cash
// Copyright (C) 2019-2020 Calin A. Culianu <calin.culianu@gmail.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program (see LICENSE.txt). If not, see
// <https://www.gnu.org/licenses/>.
//
#ifndef FULCRUM_RPC_H
#define FULCRUM_RPC_H
#include "AbstractConnection.h"
#include "Util.h"
#include <QMap>
#include <QSet>
#include <QString>
#include <QVariant>
#include <memory>
#include <optional>
#include <utility> // for std::pair
#include <variant>
namespace RPC {
/// Thrown on json that is a json object but doesn't match JSON-RPC 2.0 spec.
struct InvalidError : Util::Json::Error {
using Util::Json::Error::Error;
};
enum ErrorCodes {
/// "Parse error" ; Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.
Code_ParseError = -32700,
/// "Invalid Request" ; The JSON sent is not a valid Request object.
Code_InvalidRequest = -32600,
/// "Method not found" ; The method does not exist / is not available.
Code_MethodNotFound = -32601,
/// "Invalid params" ; Invalid method parameter(s).
Code_InvalidParams = -32602,
/// "Internal error" ; Internal JSON-RPC error.
Code_InternalError = -32603,
/// "Server error" 100 error codes that are reserved for implementation-defined server-errors.
Code_ReservedError = -32000,
/// Anything above this number is ok for us to use for application-specific errors.
Code_Custom = -31999,
/// Application-level bad request, eg request a header out of range, etc
Code_App_BadRequest = 1,
/// Daemon problem
Code_App_DaemonError = 2,
};
using KeySet = QSet<QString>;
/// this is used to lay out the protocol methods a class supports in code
/// Trivially constructible and copyable
struct Method
{
QString method; // eg 'server.ping' or 'blockchain.headers.subscribe', etc
/// If allowsRequests is false, requests for this method will return an error.
/// If allowsNotifications is false, notifications for this method will be silently ignored.
bool allowsRequests = true, allowsNotifications = false;
using PosParamRange = std::pair<unsigned, unsigned>;
static constexpr unsigned NO_POS_PARAM_LIMIT = UINT_MAX; ///< use this for PosParamsRange.second to specify no limit.
/// If this optional !has_value, then positional arguments (list for "params") are rejected.
/// Otherwise, specify an unsigned int range where .first is the minimum and .second is the maximum number
/// of positional parameters accepted. If .second is NO_POS_PARAM_LIMIT, then any number of parameters from
/// .first onward is accepted.
std::optional<PosParamRange> opt_nPosParams = PosParamRange{0, NO_POS_PARAM_LIMIT};
/// If this optional !has_value, then named arguments (dict for "params") are rejected.
/// If this optional has_value, we also accept kwargs (named args) appearing in the specified set
/// (case sensitive). (Note that a method can theoretically accept both position and kwargs if so configured).
std::optional<KeySet> opt_kwParams = {}; // '= {}' is how you specify undefined (!has_value)
/// If true, and if opt_kwParams.has_value, then we are ok with extra 'params' coming in that are not in
/// *opt_kwParams (but we still reject if keys in *opt_kwParams are missing from incoming 'params', thus
/// *opt_kwParams becomes a set of minimally required params, and we ignore everything extra if this is true).
bool allowUnknownNamedParams = false;
};
extern const QString jsonRpcVersion; ///< always "2.0"
/// An RPC message. A request, response, method call or error all use this generic struct.
/// Note this struct is cheap to copy because it uses Qt's copy-on-write containers which are fast on copy
/// because they update a shared data pointer and refct. So we don't bother wrapping this
/// in a shared_ptr or other stuff when passing it across threads, emitting it in signals, etc.
/// TODO: see if performance benefit can be gained by wrapping in shared_ptr anyway..
struct Message
{
using Id = QVariant;
// -- DATA --
Id id; ///< guaranteed to be either string, qint64, or nullptr
QString method; /**< methodName extracted from data['method'] if it was present. If this is empty then no
'method' key was present in JSON. May also contain the "matched" method on a response
object where we matched the id to a method we knew about in Connection::idMethodMap. */
QVariantMap data; ///< parsed json. 'method', 'jsonrpc', 'id', 'error', 'result', and/or 'params' get put here
bool v1 = false; ///< iff true, we parse/validate/generate based on JSON-RPC 1.0 rules, otherwise we enforce 2.0.
// -- METHODS --
/// may throw Exception. This factory method should be the way one of the 6 ways one constructs this object
static Message fromString(const QString &, Id *id_out = nullptr, bool v1 = false);
/// may throw Exception. This factory method should be the way one of the 6 ways one constructs this object
static Message fromJsonData(const QVariantMap &jsonData, Id *id_parsed_even_if_failed = nullptr, bool v1 = false);
// 4 more factories below..
/// will not throw exceptions
static Message makeError(int code, const QString & message, const Id & id = Id(), bool v1 = false);
/// will not throw exceptions
static Message makeRequest(const Id & id, const QString &methodName, const QVariantList & paramsList = QVariantList(), bool v1 = false);
static Message makeRequest(const Id & id, const QString &methodName, const QVariantMap & paramsList = QVariantMap(), bool v1 = false);
/// similar to makeRequest. A notification is just like a request but always lacking an 'id' member. This is used for asynch notifs.
static Message makeNotification(const QString &methodName, const QVariantList & paramsList = QVariantList(), bool v1 = false);
static Message makeNotification(const QString &methodName, const QVariantMap & paramsList = QVariantMap(), bool v1 = false);
/// will not throw exceptions
static Message makeResponse(const Id & reqId, const QVariant & result, bool v1 = false);
QString toJsonString() const { try {return Util::Json::toString(data, true);} catch (...) {} return QString(); }
// -- PERFORMANCE OPTIMIZATION --
// It turns out QString::QString(const char *) is called a lot in typical usase of this class, so we pre-create
// the strings we will need as static data, app-wide.
static const QString s_code; ///< "code"
static const QString s_data; ///< "data"
static const QString s_error; ///< "error"
static const QString s_id; ///< "id"
static const QString s_jsonrpc; ///< "jsonrpc"
static const QString s_message; ///< "message"
static const QString s_method; ///< "method"
static const QString s_params; ///< "params"
static const QString s_result; ///< "result"
// ./
bool isError() const {
if (!v1)
return data.contains(s_error); // v2, error= key missing unless is an actual error result.
else
return !data.value(s_error).isNull(); // v1, error=null may always be there. is error if it's not null
}
int errorCode() const { return data.value(s_error).toMap().value(s_code).toInt(); }
QString errorMessage() const { return data.value(s_error).toMap().value(s_message).toString(); }
QVariant errorData() const { return data.value(s_error).toMap().value(s_data); }
bool isRequest() const { return !isError() && hasMethod() && (hasId() && (!v1 || !id.isNull())) && !hasResult(); }
bool isResponse() const { return !isError() && hasResult() && hasId(); }
bool isNotif() const {
if (!v1)
return !isError() && !hasId() && !hasResult() && hasMethod(); // v2 notifs -- NO ID present
else
return !isError() && hasId() && id.isNull() && !hasResult() && hasMethod(); // v1 notifs.. ID present, but must be null.
}
bool hasId() const { return data.contains(s_id); }
bool hasParams() const { return data.contains(s_params); }
bool isParamsList() const { return QMetaType::Type(data.value(s_params).type()) == QMetaType::QVariantList; }
bool isParamsMap() const { return QMetaType::Type(data.value(s_params).type()) == QMetaType::QVariantMap; }
QVariant params() const { return data.value(s_params); }
QVariantList paramsList() const { return params().toList(); }
QVariantMap paramsMap() const { return params().toMap(); }
bool hasResult() const { return data.contains(s_result); }
QVariant result() const { return data.value(s_result); }
bool hasMethod() const { return data.contains(s_method); }
QString jsonRpcVersion() const { return data.value(s_jsonrpc).toString(); }
};
using MethodMap = QMap<QString, Method>;
/// A semi-concrete derived class of AbstractConnection implementing a
/// JSON-RPC based method<->result protocol. This class is client/server
/// agnostic and it just operates in terms of JSON RPC methods and results.
/// It can be used for either a client or a server.
///
/// Note that this class is somewhat transport agnostic and is intended to
/// be re-used for either HTTP or line-based (as in ElectrumX) JSON-RPC via
/// subclassing.
///
/// Concrete subclasses should implement on_readyRead() and wrapForSend().
///
/// This class just processes JSON. Subclasses implementing on_readyRead()
/// should call processJson() in this base to process the potential JSON
/// further. processJson() does validation and may implicitly close the
/// connection, etc if it doesn't like the data it received. processJson()
/// is intended to be called when the subclass things the client has sent it a
/// full "packet" of a JSON RPC message.
///
/// Note we implement a subset of JSON-RPC 2.0 which requires 'id' to
/// always be ints, strings, or null. We do not accept floats for id (the
/// JSON-RPC 2.0 spec strongly recommends against floats anyway, we are just
/// slighlty more strict than the spec).
///
/// Methods invoked on the peer need an id, and this id is used to track
/// the reply back and associate it with the method that was invoked on
/// the peer (see idMethodMap instance var).
///
/// See class Server for an example class that constructs a MethodMap and
/// passes it down.
///
/// Classes that manage rpc methods should register for the gotMessage()
/// signal and process incoming messages further. All incoming messages
/// are either requests or notifications.
///
/// gotErrorMessage can be used to receive error messages.
///
/// Note that gotMessage() won't always be emitted if the message was
/// filtered out (eg, a notification received but no "method" defined for
/// it, or a result received without a known id in idMethodMap, etc).
///
/// Server's 'Client' class derives from this.
///
class ConnectionBase : public AbstractConnection
{
Q_OBJECT
Q_PROPERTY(bool v1 READ isV1 WRITE setV1)
protected:
/// subclasses should call processJson to process what they think may be a complete json rpc message.
void processJson(const QByteArray &);
/* --
* -- Stuff subclasses must implement to make use of this class as base:
* --
*/
/// subclasses must implement this to wrap outgoing data for sending.
virtual QByteArray wrapForSend(const QByteArray &) = 0;
/* subclasses must also implement this pure virtual inherited from base:
void on_readyRead() override; */
/*
* /end
*/
public:
ConnectionBase(const MethodMap & methods, quint64 id, QObject *parent = nullptr, qint64 maxBuffer = DEFAULT_MAX_BUFFER);
~ConnectionBase() override;
const MethodMap & methods; //< Note: this map needs to remain alive for the lifetime of this connection (and all connections) .. so it should point to static or long-lived data, ideally
struct BadPeer : public Exception {
using Exception::Exception; // bring in c'tor
};
/// if peer asked for an unknown method
struct UnknownMethod : public Exception { using Exception::Exception; };
/// If peer request object was not JSON-RPC 2.0
struct InvalidRequest : public BadPeer { using BadPeer::BadPeer; };
/// If peer request object has invalid number of params
struct InvalidParameters : public BadPeer { using BadPeer::BadPeer; };
static constexpr int MAX_UNANSWERED_REQUESTS = 20000; ///< TODO: tune this down. For testing we leave this high for now.
bool isV1() const { return v1; }
void setV1(bool b) { v1 = b; }
signals:
/// call (emit) this to send a request to the peer
void sendRequest(const RPC::Message::Id & reqid, const QString &method, const QVariantList & params = QVariantList());
/// call (emit) this to send a notification to the peer
void sendNotification(const QString &method, const QVariant & params);
/// call (emit) this to send a request to the peer
void sendError(bool disconnectAfterSend, int errorCode, const QString &message, const RPC::Message::Id & reqid = Message::Id());
/// call (emit) this to send a result reply to the peer (result= message)
void sendResult(const RPC::Message::Id & reqid, const QVariant & result = QVariant());
/// this is emitted when a new message arrives that was successfully parsed and matches
/// a known method described in the 'methods' MethodMap. Unknown messages will eventually result
/// in auto-disconnect.
void gotMessage(quint64 thisId, const RPC::Message & m);
/// Same as a above, but for 'error' replies
void gotErrorMessage(quint64 thisId, const RPC::Message &em);
/// This is emitted when the peer sent malformed data to us and we didn't disconnect
/// because errorPolicy is not ErrorPolicyDisconnect
void peerError(quint64 thisId, const QString &what);
protected slots:
/// Actual implentation that prepares the request. Is connected to sendRequest() above. Runs in this object's
/// thread context. Eventually calls send() -> do_write() (from superclass).
virtual void _sendRequest(const RPC::Message::Id & reqid, const QString &method, const QVariantList & params = QVariantList());
// ditto for notifications
virtual void _sendNotification(const QString &method, const QVariant & params);
/// Actual implementation of sendError, runs in our thread context.
virtual void _sendError(bool disconnect, int errorCode, const QString &message, const RPC::Message::Id &reqid = Message::Id());
/// Actual implementation of sendResult, runs in our thread context.
virtual void _sendResult(const RPC::Message::Id & reqid, const QVariant & result = QVariant());
protected:
/// chains to base, connects sendRequest signal to _sendRequest slot
void on_connected() override;
/// Chains to base, clears idMethodMap
void on_disconnected() override;
/// adds the nRequestsSent, etc stats
Stats stats() const override;
/// map of requests that were generated via _sendRequest to method names to build a more meaningful Message
/// object (which has a .method defined even on 'result=' messages). It is an error to receive a result=
/// message from the peer with its id= parameter not having an entry in this map.
QMap<Message::Id, QString> idMethodMap;
enum ErrorPolicy {
/// Send an error RPC message on protocol errors.
/// If this is set and ErrorPolicyDisconnect is set, the disconnect will be graceful.
ErrorPolicySendErrorMessage = 1,
/// Disconnect on RPC protocol errors. If this is set along with ErrorPolicySendErrorMessage,
/// the disconnect will be graceful.
ErrorPolicyDisconnect = 2,
};
/// derived classes can set this internally (bitwise or of ErrorPolicy*)
/// to affect on_readyRead()'s behavior on peer protocol error.
int errorPolicy = ErrorPolicyDisconnect;
bool v1 = false; // if true, will generate v1 style messages and respond to v1 only
QString lastPeerError;
quint64 nRequestsSent = 0, nNotificationsSent = 0, nResultsSent = 0, nErrorsSent = 0;
quint64 nErrorReplies = 0;
};
/// Concrete class. For ElectrumX/ElectronX style JSON RPC where newlines delimit RPC messages.
class LinefeedConnection : public ConnectionBase {
public:
using ConnectionBase::ConnectionBase;
~LinefeedConnection() override; ///< for vtable
protected:
/// implements pure virtual from super to handle linefeed-based JSON. When a full line arrives, calls ConnectionBase::processJson
void on_readyRead() override;
QByteArray wrapForSend(const QByteArray &) override;
};
/// JSON RPC over HTTP. Wraps the outgoing data in headers and can also parse incoming headers.
/// For use by the bitcoind rpc mechanism.
class HttpConnection : public ConnectionBase {
Q_OBJECT
public:
using ConnectionBase::ConnectionBase;
~HttpConnection() override; ///< for vtable
void setAuth(const QString & username, const QString & password);
void clearAuth() { authCookie.clear(); }
//static void Test();
signals:
/// emitted when the other side (usually bitcoind) didn't accept our auth cookie.
void authFailure(HttpConnection *me);
protected:
void on_readyRead() override;
QByteArray wrapForSend(const QByteArray &) override;
private:
QByteArray authCookie;
struct StateMachine;
using SMDel = std::function<void(StateMachine *)>;
std::unique_ptr<StateMachine, SMDel> sm; ///< we need to declare this with a deleter otherwise subclasses won't be able to inherit from us because StateMachine is a private, opaque struct; the need for a deleter is due to implementation details of how unique_ptr works with opaque types.
};
} // end namespace RPC
/// So that Qt signal/slots work with this type. Metatypes are also registered at startup via qRegisterMetatype
Q_DECLARE_METATYPE(RPC::Message);
Q_DECLARE_METATYPE(RPC::Message::Id);
#endif // FULCRUM_RPC_H