forked from cculianu/Fulcrum
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathRPC.h
379 lines (323 loc) · 19.4 KB
/
RPC.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
//
// 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/>.
//
#pragma once
#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);
/// Note in pathological cases bad_alloc may be thrown here, so we just return an empty QString in that case and hope for the best.
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 thinks 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);