1 /** 
2 * HiBON Remote Pprocedure Call
3 */
4 module tagion.communication.HiRPC;
5 
6 import std.exception : assumeWontThrow;
7 import std.format;
8 import std.traits : EnumMembers;
9 import tagion.basic.Types : Buffer;
10 import tagion.basic.tagionexceptions : Check;
11 import tagion.crypto.SecureInterfaceNet : SecureNet;
12 import tagion.crypto.Types : Pubkey, Signature;
13 import tagion.hibon.Document : Document;
14 import tagion.hibon.HiBON : HiBON;
15 import tagion.hibon.HiBONException;
16 import tagion.hibon.HiBONJSON;
17 import tagion.hibon.HiBONRecord;
18 
19 /// HiRPC format exception
20 @safe
21 class HiRPCException : HiBONException {
22     this(string msg, string file = __FILE__, size_t line = __LINE__) pure {
23         super(msg, file, line);
24     }
25 }
26 
27 /// UDA to make a RPC member
28 enum HiRPCMethod;
29 
30 private static string[] _Callers(T)() {
31     import std.meta : ApplyRight, Filter;
32     import std.traits : hasUDA, isCallable;
33 
34     string[] result;
35     static foreach (name; __traits(derivedMembers, T)) {
36         {
37             alias Overloads = __traits(getOverloads, T, name);
38             static if (Overloads.length) {
39                 alias hasMethod = ApplyRight!(hasUDA, HiRPCMethod);
40                 static foreach (i; 0 .. Overloads.length) {
41                     static if (hasUDA!(Overloads[i], HiRPCMethod)) {
42                         result ~= name;
43                     }
44                 }
45             }
46         }
47     }
48     return result;
49 }
50 
51 enum Callers(T) = _Callers!T();
52 
53 /// HiRPC handler
54 @safe
55 struct HiRPC {
56     import tagion.hibon.HiBONRecord;
57 
58     /// HiRPC call method 
59     struct Method {
60         @optional @(filter.Initialized) uint id; /// RPC identifier
61         @optional @filter(q{!a.empty}) Document params; /// RPC arguments
62         @label("method") @(inspect.Initialized) string name; /// RPC method name
63 
64         mixin HiBONRecord;
65     }
66     /// HiRPC result from a method
67     struct Response {
68         @optional @(filter.Initialized) uint id; /// RPC response id, if given by the method
69         Document result; /// Return data from the method request
70         mixin HiBONRecord;
71     }
72 
73     /// HiRPC error response for a method
74     struct Error {
75         @optional @(filter.Initialized) uint id; /// RPC response id, if given by the method 
76         @label("data") @optional @filter(q{!a.empty}) Document data; /// Optional error response package
77         @label("text") @optional @(filter.Initialized) string message; /// Optional Error text message
78         @label("code") @optional @(filter.Initialized) int code; /// Optional error code
79 
80         static bool valid(const Document doc) {
81             enum codeName = GetLabel!(code).name;
82             enum messageName = GetLabel!(message).name;
83             enum dataName = GetLabel!(data).name;
84             return doc.hasMember(codeName) || doc.hasMember(messageName) || doc.hasMember(dataName);
85         }
86 
87         mixin HiBONRecord;
88     }
89 
90     /// Get the id of the document doc
91     /// Params:
92     ///   doc = Method, Response or Error document.
93     /// Returns: RPC id if given or else return id 0
94     static uint getId(const Document doc) nothrow {
95         enum idLabel = GetLabel!(Error.id).name;
96         if (doc.hasMember(idLabel)) {
97             return assumeWontThrow(doc[idLabel].get!uint);
98         }
99         return uint.init;
100     }
101 
102     /// Check if is T is a message
103     /// Params: T is message data type
104     /// Returns: true if T is HiRPC message type
105     enum isMessage(T) = is(T : const(Method)) || is(T : const(Response)) || is(T : const(Error));
106 
107     /// State of the signature in the HiRPC 
108     enum SignedState {
109         INVALID = -1, /// Incorrect signature
110         NOSIGN = 0, /// HiRPC has no signature
111         VALID = 1 /// HiRPC was signed correctly
112     }
113 
114     /// Message type
115     enum Type : uint {
116         none, /// No valid Type
117         method, /// HiRPC Action method
118         result, /// HiRPC Respose message
119         error /// HiRPC Error message
120     }
121 
122     /// HiRPC Post direction
123     enum Direction {
124         SEND, /// Marks the HiRPC Post as a sender type
125         RECEIVE /// Marks the HiRPC Post as a receiver type
126     }
127 
128     /// get the message to of the message
129     /// Params: T message data type
130     /// Returns: The type of the HiRPC message 
131     static Type getType(T)(const T message) if (isHiBONRecord!T) {
132         static if (is(T : const(Method))) {
133             return Type.method;
134         }
135         else static if (is(T : const(Response))) {
136             return Type.result;
137         }
138         else static if (is(T : const(Error))) {
139             return Type.error;
140         }
141         else {
142             return getType(message.toDoc);
143         }
144     }
145 
146     /// Ditto 
147     static Type getType(const Document doc) {
148         import std.conv : to;
149 
150         enum messageName = GetLabel!(Sender.message).name;
151         const message_doc = doc[messageName].get!Document;
152         if (message_doc.hasMember(GetLabel!(Method.name).name)) {
153             return Type.method;
154         }
155         if (message_doc.hasMember(GetLabel!(Response.result).name)) {
156             return Type.result;
157         }
158         return Type.error;
159     }
160 
161     /// HiRPC Post (Sender,Receiver)
162     @recordType("HiRPC")
163     struct Post(Direction DIRECTION) {
164         union Message {
165             Method method;
166             Response response;
167             Error error;
168             uint id;
169         }
170 
171         static assert(Message.method.id.alignof == Message.id.alignof);
172         static assert(Message.response.id.alignof == Message.id.alignof);
173         static assert(Message.error.id.alignof == Message.id.alignof);
174 
175         @label("$sign") @optional @(filter.Initialized) Signature signature; /// Signature of the message
176         @label("$Y") @optional @(filter.Initialized) Pubkey pubkey; /// Owner key of the message
177         @label("$msg") Document message; /// the HiRPC message
178         @exclude immutable Type type;
179 
180         @nogc const pure nothrow {
181             /// Returns: true if the message is a method
182             bool isMethod() {
183                 return type is Type.method;
184             }
185             /// Returns: true if the message is a response
186             bool isResponse() {
187                 return type is Type.result;
188             }
189 
190             /// Returns: true of the message is an error
191             bool isError() {
192                 return type is Type.error;
193             }
194         }
195 
196         bool supports(T)() const {
197             import std.algorithm.searching : canFind;
198             import std.traits : isCallable;
199 
200             return (type is Type.method) &&
201                 Callers!T.canFind(method.name);
202         }
203 
204         bool verify(const Document doc) {
205             if (pubkey.length) {
206                 check(signature.length !is 0, "Message Post has a public key without signature");
207             }
208             return true;
209         }
210 
211         static if (DIRECTION is Direction.RECEIVE) {
212             @exclude protected Message _message;
213             @exclude immutable SignedState signed;
214             enum signName = GetLabel!(signature).name;
215             enum pubkeyName = GetLabel!(pubkey).name;
216             enum messageName = GetLabel!(message).name;
217             this(const Document doc) {
218                 this(null, doc);
219             }
220 
221             this(const SecureNet net, const Document doc, Pubkey pkey = Pubkey.init)
222             in {
223                 if (signature.length) {
224                     assert(net !is null, "The signature can't be veified because the SecureNet is missing");
225                 }
226             }
227             do {
228                 check(!doc.hasHashKey, "Document containing hashkey can not be used as a message in HiPRC");
229 
230                 type = getType(doc);
231                 message = doc[messageName].get!Document;
232                 signature = doc.hasMember(signName) ? doc[signName].get!(Signature) : Signature.init;
233                 pubkey = doc.hasMember(pubkeyName) ? doc[pubkeyName].get!(Pubkey) : pkey;
234                 Pubkey used_pubkey;
235                 static SignedState verifySignature(const SecureNet net, const Document doc, const Signature sgn, const Pubkey pkey) {
236                     if (sgn.length) {
237                         //immutable fingerprint=net.hashOf(msg);
238                         if (net is null) {
239                             return SignedState.INVALID;
240                         }
241                         Pubkey used_pubkey = pkey;
242                         if (!used_pubkey.length) {
243                             used_pubkey = net.pubkey;
244                         }
245                         if (net.verify(doc, sgn, pkey)) {
246                             return SignedState.VALID;
247                         }
248                         else {
249                             return SignedState.INVALID;
250                         }
251                     }
252                     return SignedState.NOSIGN;
253                 }
254 
255                 void set_message() @trusted {
256                     with (Type) {
257                         final switch (type) {
258                         case none:
259                             check(0, "Invalid HiPRC message");
260                             break;
261                         case method:
262                             _message.method = Method(message);
263                             break;
264                         case result:
265                             _message.response = Response(message);
266                             break;
267                         case error:
268                             _message.error = Error(message);
269                         }
270                     }
271                 }
272 
273                 set_message;
274                 signed = verifySignature(net, message, signature, pubkey);
275             }
276 
277             this(T)(const SecureNet net, T pack) if (isHiBONRecord!T) {
278                 this(net, pack.toDoc);
279             }
280 
281             /** 
282              * 
283              * Returns: if the message type is an error it returns it
284             * or else it throws an exception
285              */
286             @trusted const(Error) error() const pure {
287                 check(type is Type.error, format("Message type %s expected not %s", Type.error, type));
288                 return _message.error;
289             }
290 
291             /** 
292              * 
293              * Returns: if the message type is an response it returns it
294             * or else it throws an exception
295              */
296             @trusted const(Response) response() const pure {
297                 check(type is Type.result, format("Message type %s expected not %s", Type.result, type));
298                 return _message.response;
299             }
300 
301             /** 
302             * 
303             * Returns: if the message type is an response it returns it
304             * or else it throws an exception
305             */
306             @trusted const(Method) method() const pure {
307                 check(type is Type.method, format("Message type %s expected not %s", Type.method, type));
308                 return _message.method;
309             }
310 
311             @trusted
312             bool isRecord(T)() const {
313                 with (Type) {
314                     final switch (type) {
315                     case none, error:
316                         return false;
317                     case method:
318                         return T.isRecord(_message.method.params);
319                     case result:
320                         return T.isRecord(_message.response.result);
321                     }
322                 }
323                 assert(0);
324             }
325         }
326         else {
327             this(T)(const SecureNet net, const T post) if (isHiBONRecord!T || is(T : const Document)) {
328                 static if (isHiBONRecord!T) {
329                     message = post.toDoc;
330                 }
331                 else {
332                     message = post;
333                 }
334                 type = getType(post);
335                 if (net !is null) {
336                     // immutable signed=net.sign(message);
337                     // fingerprint=signed.message;
338                     signature = net.sign(message).signature;
339                     pubkey = net.pubkey;
340                 }
341             }
342 
343             Error error() const
344             in {
345                 assert(type is Type.error, format("Message type %s expected not %s", Type.error, type));
346             }
347             do {
348                 return Error(message);
349             }
350 
351             Response response() const
352             in {
353                 assert(type is Type.result, format("Message type %s expected not %s", Type.result, type));
354             }
355             do {
356                 return Response(message);
357             }
358 
359             Method method() const
360             in {
361                 assert(type is Type.method, format("Message type %s expected not %s", Type.method, type));
362             }
363             do {
364                 return Method(message);
365             }
366 
367             /++
368              Checks if the message has been signed
369              NOTE!! This does not mean that the signature is correct
370              Returns:
371              True if the message has been signed
372              +/
373             @nogc bool isSigned() const pure nothrow {
374                 return (signature.length !is 0);
375             }
376         }
377 
378         /** 
379              * Create T with the method params and the arguments.
380              *  T(args, method.param)
381              * Params:
382              *   args = arguments to the
383              * Returns: the constructed T
384              */
385         const(T) params(T, Args...)(Args args) const if (isHiBONRecord!T) {
386             check(type is Type.method, format("Message type %s expected not %s", Type.method, type));
387             return T(args, method.params);
388         }
389 
390         Document params() const {
391             check(type is Type.method, format("Message type %s expected not %s", Type.method, type));
392             return method.params;
393         }
394 
395         const(T) result(T, Args...)(Args args) const if (isHiBONRecord!T) {
396             check(type is Type.result, format("Message type %s expected not %s", Type.result, type));
397             return T(response.result, args);
398         }
399 
400         Document result() const {
401             check(type is Type.result, format("Message type %s expected not %s", Type.result, type));
402 
403             return response.result;
404         }
405 
406         mixin HiBONRecord!("{}");
407     }
408 
409     alias Sender = Post!(Direction.SEND);
410     alias Receiver = Post!(Direction.RECEIVE);
411 
412     alias check = Check!HiRPCException;
413     const SecureNet net;
414 
415     /**
416      * Generate a random id 
417      * Returns: random id
418     **/
419     const(uint) generateId() @safe const {
420         import rnd = tagion.utils.Random;
421 
422         return rnd.generateId;
423     }
424 
425     /** 
426      * Creates a sender via opDispatch.method with argument params
427      * Params:
428      *   method = opDispatch method name
429      *   params = argument for method
430      *   id = optional id
431      * Returns: The created sender
432      */
433     immutable(Sender) opDispatch(string method, T)(
434             ref auto const T params,
435             const uint id = uint.max) const {
436         return action(method, params, id);
437     }
438 
439     /** 
440      * Creates a sender with a runtime method name 
441      * Params:
442      *   method = method name 
443      *   params = argument for the method
444      *   id = opitional id
445      * Returns: 
446      */
447     immutable(Sender) action(string method, const Document params, const uint id = uint.max) const {
448         Method message;
449         message.id = (id is uint.max) ? generateId : id;
450         if (!params.empty) {
451             message.params = params;
452         }
453         message.name = method;
454         message.params = params;
455         auto sender = Sender(net, message);
456         return sender;
457     }
458 
459     /// Ditto
460     immutable(Sender) action(T)(string method, T params, const uint id = uint.max) const
461     if (isHiBONRecord!T) {
462         return action(method, params.toDoc, id);
463     }
464 
465     /// Ditto
466     immutable(Sender) action(string method, const(HiBON) params = null, const uint id = uint.max) const {
467         const doc = Document(params);
468         return action(method, doc, id);
469     }
470 
471     /**
472      * Create a return sender including the return value
473      * return_value:
474      *   receiver = HiRPC receiver
475      *   return_value = return value from method 
476      * Returns:
477      *   Response sender to be return to the caller
478      */
479     immutable(Sender) result(ref const(Receiver) receiver, const Document return_value) const {
480         Response message;
481         message.id = receiver.method.id;
482         message.result = return_value;
483         immutable sender = Sender(net, message);
484         return sender;
485     }
486 
487     /// Ditto
488     immutable(Sender) result(T)(ref const(Receiver) receiver, T return_value) const
489     if (isHiBONRecord!T) {
490         return result(receiver, return_value.toDoc);
491     }
492 
493     /// Ditto
494     immutable(Sender) result(ref const(Receiver) receiver, const(HiBON) return_value) const {
495         return result(receiver, Document(return_value));
496     }
497 
498     /**
499      * Creates error response sender from a receiver 
500      * Params:
501      *   receiver = HiRPC receiver 
502      *   msg = error text message
503      *   code = error code
504      *   data = error data load
505      * Returns: 
506      *  Response error sender
507      */
508     immutable(Sender) error(ref const(Receiver) receiver, string msg, const int code = 0, Document data = Document()) const {
509         return error(receiver.method.id, msg, code, data);
510     }
511 
512     /// Ditto
513     immutable(Sender) error(const uint id, string msg, const int code = 0, Document data = Document()) const {
514         Error message;
515         message.id = id;
516         message.code = code;
517         message.data = data;
518         message.message = msg;
519         return Sender(net, message);
520     }
521 
522     /**
523      * Creates a receiver from a Document doc 
524      * Params:
525      *   doc = HiBON Document
526      * Returns: 
527      *   A checked receiver
528      */
529     final immutable(Receiver) receive(Document doc) const {
530         auto receiver = Receiver(net, doc);
531         return receiver;
532     }
533 
534     /// Ditto
535     final immutable(Receiver) receive(T)(T sender) const if (isHiBONRecord!T) {
536         auto receiver = Receiver(net, sender.toDoc);
537         return receiver;
538     }
539 }
540 
541 @safe
542 @recordType("OK")
543 struct ResultOk {
544     mixin HiBONRecord!();
545 }
546 
547 ///
548 unittest {
549     import tagion.crypto.SecureNet : BadSecureNet, StdSecureNet;
550     import tagion.hibon.HiBONRecord;
551     import tagion.crypto.secp256k1.NativeSecp256k1;
552 
553     class HiRPCNet : StdSecureNet {
554         this(string passphrase) {
555             super();
556             generateKeyPair(passphrase);
557         }
558     }
559 
560     immutable passphrase = "Very secret password for the server";
561     enum func_name = "func_name";
562 
563     {
564         HiRPC hirpc = HiRPC(new HiRPCNet(passphrase));
565         HiRPC bad_hirpc = HiRPC(new BadSecureNet(passphrase));
566         auto params = new HiBON;
567         params["test"] = 42;
568         // Create a send method name func_name and argument params
569         const sender = hirpc.action(func_name, params);
570         // Sender with bad credetials
571         const invalid_sender = bad_hirpc.action(func_name, params, sender.method.id);
572 
573         const doc = sender.toDoc;
574         const invalid_doc = invalid_sender.toDoc;
575 
576         // Convert the do to a received HiRPC
577         const receiver = hirpc.receive(doc);
578         const invalid_receiver = hirpc.receive(invalid_doc);
579 
580         assert(receiver.method.id is sender.method.id);
581         assert(receiver.method.name == sender.method.name);
582         // Check that the received HiRPC is sigen correctly
583         assert(receiver.signed is HiRPC.SignedState.VALID);
584 
585         assert(invalid_receiver.method.id is sender.method.id);
586         assert(invalid_receiver.method.name == sender.method.name);
587         assert(invalid_receiver.signed is HiRPC.SignedState.INVALID);
588 
589         static struct ResultStruct {
590             int x;
591             mixin HiBONRecord;
592         }
593 
594         { // Response
595             auto hibon = new HiBON;
596             hibon["x"] = 42;
597             const send_back = hirpc.result(receiver, hibon);
598             const result = ResultStruct(send_back.response.result);
599             assert(result.x is 42);
600         }
601 
602         { // Error
603             const send_error = hirpc.error(receiver, "Some error", -1);
604             assert(send_error.error.message == "Some error");
605             assert(send_error.error.code == -1);
606             assert(send_error.isSigned);
607         }
608     }
609 
610     {
611         HiRPC hirpc;
612         { /// Unsigend message (no permission)
613             HiBON t = new HiBON();
614             t["$test"] = 5;
615 
616             const sender = hirpc.action("action", t);
617 
618             auto test2 = sender.toDoc;
619             // writeln(test2.toJSON);
620             // writefln("sender.isSigned=%s", sender.isSigned);
621             assert(!sender.isSigned, "This message is un-sigend, which is fine because the HiRPC does not contain a SecureNet");
622             {
623                 const receiver = hirpc.receive(sender.toDoc);
624                 // writefln("receiver=%s", receiver);
625                 assert(receiver.method.id is sender.method.id);
626                 // writefln("receiver.method.name is sender.method.name", receiver.method.name, sender.method.name);
627                 assert(receiver.method.name == sender.method.name);
628                 assert(receiver.signed is HiRPC.SignedState.NOSIGN);
629 
630                 const params = receiver.method.params;
631                 assert(params["$test"].get!int  is 5);
632             }
633         }
634         // writefln("recever.verified=%s", recever.verified);
635     }
636 }