1 /**
2     Core functionnalities of the RPC framework.
3 
4     Copyright: © 2018 Eliott Dumeix
5     License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 */
7 module rpc.core;
8 
9 import std.traits : hasUDA;
10 import vibe.internal.meta.uda : onlyAsUda;
11 
12 version(RpcUnitTest) { public import unit_threaded; }
13 else { enum ShouldFail; } // so production builds compile
14 
15 
16 // ////////////////////////////////////////////////////////////////////////////
17 // Attributes																 //
18 // ////////////////////////////////////////////////////////////////////////////
19 package struct NoRpcMethodAttribute
20 {
21 }
22 
23 /// Methods marked with this attribute will not be treated as rpc endpoints.
24 @property NoRpcMethodAttribute noRpcMethod() @safe
25 {
26     if (!__ctfe)
27         assert(false, onlyAsUda!__FUNCTION__);
28     return NoRpcMethodAttribute();
29 }
30 
31 ///
32 unittest
33 {
34     interface IAPI
35     {
36         @noRpcMethod
37         void submit();
38     }
39 }
40 
41 package struct RpcMethodAttribute
42 {
43     string method;
44 }
45 
46 /// Methods marked with this attribute will be treated as rpc endpoints.
47 /// Params:
48 ///     method = RPC method name
49 RpcMethodAttribute rpcMethod(string method) @safe
50 {
51     if (!__ctfe)
52         assert(false, onlyAsUda!__FUNCTION__);
53     return RpcMethodAttribute(method);
54 }
55 
56 ///
57 unittest
58 {
59     interface IAPI
60     {
61         @rpcMethod("do_submit")
62         void submit();
63     }
64 }
65 
66 /// Allow to specify the id type used by some rpc protocol (like json-rpc 2.0)
67 package struct RpcIdTypeAttribute(T) if (is(T == int) || is(T == string))
68 {
69     alias idType = T;
70 }
71 alias rpcIdType(T) = RpcIdTypeAttribute!T;
72 
73 /// attributes utils
74 private enum isRpcMethod(alias M) = !hasUDA!(M, NoRpcMethodAttribute);
75 
76 /// On a rpc method, when RpcMethodObjectParams.asObject is selected, this
77 /// attribute is used to customize the name rendered for each arg in the params object.
78 package struct RpcMethodObjectParams
79 {
80     string[string] names;
81 }
82 
83 /// Methods marked with this attribute will see its parameters rendered as an object (if applicable by the protocol).
84 RpcMethodObjectParams rpcObjectParams() @safe
85 {
86     if (!__ctfe)
87         assert(false, onlyAsUda!__FUNCTION__);
88     return RpcMethodObjectParams();
89 }
90 
91 ///
92 unittest
93 {
94     interface IAPI
95     {
96         @rpcObjectParams
97         void submit(string hash);
98         // In json-rpc params will be rendered as: "params": {"hash": "dZf4F"}
99     }
100 }
101 
102 /// ditto
103 RpcMethodObjectParams rpcObjectParams(string[string] names) @safe
104 {
105     if (!__ctfe)
106         assert(false, onlyAsUda!__FUNCTION__);
107     return RpcMethodObjectParams(names);
108 }
109 
110 ///
111 unittest
112 {
113     interface IAPI
114     {
115         @rpcObjectParams(["hash": "hash_renamed"])
116         void submit(string hash);
117         // In json-rpc params will be rendered as: "params": {"hash_renamed": "dZf4F"}
118     }
119 }
120 
121 // Attribute to force params to be sended as array (even if alone)
122 package struct RpcArrayParams
123 {
124 }
125 
126 /// Methods marked with this attribute will see its parameters rendered as an array (if applicable by the protocol).
127 @property RpcArrayParams rpcArrayParams() @safe
128 {
129     if (!__ctfe)
130         assert(false, onlyAsUda!__FUNCTION__);
131     return RpcArrayParams();
132 }
133 
134 ///
135 unittest
136 {
137     interface IAPI
138     {
139         @rpcArrayParams
140         void submit(string hash);
141         // In json-rpc params will be rendered as: "params": ["dZf4F"]
142     }
143 }
144 
145 /// attributes utils
146 package enum hasRpcArrayParams(alias M) = hasUDA!(M, RpcArrayParams);
147 
148 
149 /** Hold settings to be used by the rpc interface.
150 */
151 class RpcInterfaceSettings
152 {
153     import core.time;
154 
155 public:
156     /** Ignores a trailing underscore in method and function names.
157         With this setting set to $(D true), it's possible to use names in the
158         REST interface that are reserved words in D.
159     */
160     bool stripTrailingUnderscore = true;
161 
162     Duration responseTimeout = 500.msecs;
163 
164     string linesep = "\n";
165 
166     /** Optional handler used to render custom replies in case of errors.
167     */
168     RpcErrorHandler errorHandler;
169 }
170 
171 ///
172 alias RpcErrorHandler = void delegate(Exception e) @safe nothrow;
173 
174 /** Define an id generator.
175 
176     Template_Params:
177         TId = The type used to identify rpc request.
178 */
179 package interface IIdGenerator(TId)
180 {
181     TId getNextId() @safe nothrow;
182 }
183 
184 /** An int id generator.
185 */
186 package class IdGenerator(TId: int): IIdGenerator!TId
187 {
188     private TId _id;
189 
190     override TId getNextId() @safe nothrow
191     {
192         _id++;
193         return _id;
194     }
195 }
196 
197 /** A string id generator.
198 */
199 package class IdGenerator(TId: string): IIdGenerator!TId
200 {
201     import std..string : succ;
202 
203     private TId _id = "0";
204 
205     override  TId getNextId() @safe nothrow
206     {
207         _id = succ(_id);
208         return _id;
209     }
210 }
211 
212 /** An RPC request identified by an id of type TId.
213 */
214 package interface IRpcRequest(TId)
215 {
216     /// Get the request id
217     @property TId requestId();
218 }
219 
220 /// An RPC response.
221 package interface IRpcResponse
222 {
223     string toString() @safe;
224 }
225 
226 /**
227     An RPC client working with TRequest and TResponse.
228 */
229 interface IRpcClient(TId, TRequest, TResponse)
230     if (is(TRequest: IRpcRequest!TId) && is(TResponse: IRpcResponse))
231 {
232     import core.time : Duration;
233 
234     /// Returns true if the client is connected.
235     @property bool connected() @safe nothrow;
236 
237     /// Try to connect the client.
238     bool connect() @safe nothrow;
239 
240     /**
241         Send a request and wait a response for the specified timeout.
242 
243         Params:
244             request = The request to send.
245             timeout = How mush to wait for a response.
246 
247         Throws:
248             Any of RPCException sub-classes.
249     */
250     TResponse sendRequestAndWait(TRequest request, Duration timeout = Duration.max()) @safe;
251 
252     /// Tell to process the input stream once.
253     void tick() @safe;
254 }
255 
256 /**
257     A raw rpc client sending TRequest and receiving TResponse object through
258     Input/Output stream.
259 
260     Template_Params:
261         TId = The type used to identify rpc request.
262         TRequest = Request type, must be an IRPCRequest.
263         TResponse = Reponse type, must be an IRPCResponse.
264 */
265 abstract class RawRpcClient(TId, TRequest, TResponse): IRpcClient!(TId, TRequest, TResponse)
266 {
267     import vibe.core.stream: InputStream, OutputStream;
268 
269     protected OutputStream _ostream;
270     protected InputStream _istream;
271 
272     this(OutputStream ostream, InputStream istream) @safe
273     {
274         _ostream = ostream;
275         _istream = istream;
276     }
277 
278     @disable @property bool connected() @safe nothrow { return true; }
279     @disable bool connect() @safe nothrow { return true; }
280     override void tick() @safe { }
281 }
282 
283 /**
284     Base implementation of an Http RPC client.
285 
286     Template_Params:
287         TId = The type used to identify rpc request.
288         TRequest = Request type, must be an IRPCRequest.
289         TResponse = Reponse type, must be an IRPCResponse.
290 */
291 
292 class HttpRpcClient(TId, TRequest, TResponse): IRpcClient!(TId, TRequest, TResponse)
293 {
294     import vibe.data.json;
295     import vibe.http.client;
296     import vibe.stream.operations;
297     import std.conv: to;
298     import vibe.core.log;
299 
300 private:
301     string _url;
302     IIdGenerator!TId _idGenerator;
303     TResponse[TId] _pendingResponse;
304 
305 public:
306     this(string url)
307     {
308         _url = url;
309         _idGenerator = new IdGenerator!TId();
310     }
311 
312     override TResponse sendRequestAndWait(TRequest request, Duration timeout = Duration.max()) @safe
313     {
314         request.id = _idGenerator.getNextId();
315 
316         TResponse reponse;
317 
318         requestHTTP(_url,
319             (scope req) {
320                 req.method = HTTPMethod.POST;
321 
322                 req.writeJsonBody(request);
323                 logTrace("client request: %s", request);
324             },
325             (scope res) {
326                 if (res.statusCode == 200)
327                 {
328                     string json = res.bodyReader.readAllUTF8();
329                     logTrace("client response: %s", json);
330                     reponse = deserializeJson!TResponse(json);
331                 }
332                 else
333                 {
334                     throw new RpcTimeoutException("HTTP " ~ to!string(res.statusCode) ~ ": " ~ res.statusPhrase);
335                 }
336             }
337         );
338 
339         return reponse;
340     }
341 
342     @disable @property bool connected() { return true; }
343     @disable bool connect() @safe nothrow { return true; }
344     @disable void tick() @safe nothrow { }
345 }
346 
347 /**
348     Represent server to client stream.
349 
350     Template_Params:
351         TResponse = Reponse type, must be an IRPCResponse.
352 */
353 interface IRpcServerOutput(TResponse: IRpcResponse)
354 {
355     void sendResponse(TResponse reponse) @safe;
356 }
357 
358 /// A RPC request handler
359 alias RpcRequestHandler(TRequest, TResponse) = void delegate(TRequest req, IRpcServerOutput!TResponse serv) @safe;
360 
361 /** An RPC server that can register handler.
362 
363     Template_Params:
364         TId = The type used to identify RPC request.
365 
366         TRequest = Request type, must be an IRPCRequest.
367 
368         TResponse = Reponse type, must be an IRPCResponse.
369 */
370 interface IRpcServer(TId, TRequest, TResponse)
371     if (is(TRequest: IRpcRequest!TId) && is(TResponse: IRpcResponse))
372 {
373     /** Register a delegate to be called on reception of a request matching 'method'.
374 
375         Params:
376             method = The RPC method to match.
377             handler = The delegate to call.
378     */
379     void registerRequestHandler(string method, RpcRequestHandler!(TRequest, TResponse) handler);
380 
381     /** Auto-register all method in an interface.
382 
383         Template_Params:
384             TImpl = The interface type.
385 
386         Params:
387             instance = The interface instance.
388             settings = Optional RPC settings.
389     */
390     void registerInterface(TImpl)(TImpl instance, RpcInterfaceSettings settings = null);
391 
392     void tick() @safe;
393 }
394 
395 
396 abstract class RawRpcServer(TId, TRequest, TResponse): IRpcServer!(TId, TRequest, TResponse)
397 {
398     import vibe.core.stream: InputStream, OutputStream;
399 
400     protected OutputStream _ostream;
401     protected InputStream _istream;
402 
403     this(OutputStream ostream, InputStream istream) @safe
404     {
405         _ostream = ostream;
406         _istream = istream;
407     }
408 }
409 
410 /** An HTTP RPC server.
411 */
412 class HttpRpcServer(TId, TRequest, TResponse): IRpcServer!(TId, TRequest, TResponse)
413 {
414     import vibe.core.log;
415     import vibe.data.json: Json, parseJson, deserializeJson;
416     import vibe.http.router;
417     import vibe.stream.operations;
418     public import vibe.http.server : HTTPServerResponse;
419 
420     alias RpcRespHandler = IRpcServerOutput!TResponse;
421     alias RequestHandler = RpcRequestHandler!(TRequest, TResponse);
422 
423 private:
424     URLRouter _router;
425     RequestHandler[string] _requestHandler;
426 
427 public:
428     this(URLRouter router, string path)
429     {
430         _router = router;
431         _router.post(path, &onPostRequest);
432     }
433 
434     @disable void registerInterface(I)(I instance, RpcInterfaceSettings settings = null)
435     {
436     }
437 
438     void registerRequestHandler(string method, RequestHandler handler)
439     {
440         _requestHandler[method] = handler;
441     }
442 
443 protected:
444     /** Handle all HTTP POST request on the RPC route and
445         forward call to the service.
446     */
447     void onPostRequest(HTTPServerRequest req, HTTPServerResponse res)
448     {
449         string json = req.bodyReader.readAllUTF8();
450         logTrace("post request received: %s", json);
451 
452         this.process(json, createReponseHandler(res));
453     }
454 
455     /// Creates a new response handler.
456     abstract RpcRespHandler createReponseHandler(HTTPServerResponse res) @safe nothrow;
457 
458     void process(string data, RpcRespHandler respHandler)
459     @safe nothrow {
460         try
461         {
462             Json json = parseJson(data);
463 
464             void process(Json jsonObject)
465             @safe {
466                 auto request = deserializeJson!TRequest(jsonObject);
467                 if (request.method in _requestHandler)
468                 {
469                     _requestHandler[request.method](request, respHandler);
470                 }
471             }
472 
473             // batch of commands
474             if (json.type == Json.Type.array)
475             {
476                 foreach (object; json.byValue)
477                 {
478                     process(object);
479                 }
480             }
481             else
482             {
483                 process(json);
484             }
485         }
486         catch (Exception e)
487         {
488             // request parse error, so send a response without id
489             auto response = buildResponseFromException(e);
490             try {
491                 respHandler.sendResponse(response);
492             } catch (Exception e) {
493                 logCritical("unable to send response: %s", e.msg);
494                 // TODO: add a delgate to allow the user to handle error
495             }
496         }
497     }
498 
499     abstract TResponse buildResponseFromException(Exception e) @safe nothrow;
500 }
501 
502 
503 /// Base class for RPC exceptions.
504 class RpcException: Exception {
505     public Exception inner;
506 
507     this(string msg, Exception inner = null)
508     @safe {
509         super(msg);
510         this.inner = inner;
511     }
512 }
513 
514 /// Client not connected exception
515 class RpcNotConnectedException: RpcException {
516     this(string msg)
517     @safe {
518         super(msg);
519     }
520 }
521 
522 /// Parsing exception.
523 class RpcParsingException: RpcException {
524     this(string msg, Exception inner = null)
525     @safe {
526         super(msg, inner);
527     }
528 }
529 
530 /// Unhandled RPC method on server-side.
531 class UnhandledRpcMethod: RpcException
532 {
533     this(string msg)
534     @safe {
535         super(msg);
536     }
537 }
538 
539 /// RPC call timeout on client-side.
540 class RpcTimeoutException: RpcException
541 {
542     this(string msg)
543     @safe {
544         super(msg);
545     }
546 }