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 }