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 }