DefaultRpcInvoker.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Threading.Tasks;
  7. using EdjCase.JsonRpc.Core;
  8. using EdjCase.JsonRpc.Router.Abstractions;
  9. using Microsoft.Extensions.Logging;
  10. using Newtonsoft.Json;
  11. using Newtonsoft.Json.Linq;
  12. using EdjCase.JsonRpc.Router.Utilities;
  13. using Microsoft.AspNetCore.Authorization;
  14. using Microsoft.AspNetCore.Http;
  15. using Microsoft.Extensions.Options;
  16. using Microsoft.Extensions.DependencyInjection;
  17. using EdjCase.JsonRpc.Router.MethodProviders;
  18. using System.Collections.Concurrent;
  19. namespace EdjCase.JsonRpc.Router.Defaults
  20. {
  21. /// <summary>
  22. /// Default Rpc method invoker that uses asynchronous processing
  23. /// </summary>
  24. public class DefaultRpcInvoker : IRpcInvoker
  25. {
  26. /// <summary>
  27. /// Logger for logging Rpc invocation
  28. /// </summary>
  29. private ILogger<DefaultRpcInvoker> logger { get; }
  30. /// <summary>
  31. /// AspNet service to authorize requests
  32. /// </summary>
  33. private IAuthorizationService authorizationService { get; }
  34. /// <summary>
  35. /// Provides authorization policies for the authroziation service
  36. /// </summary>
  37. private IAuthorizationPolicyProvider policyProvider { get; }
  38. /// <summary>
  39. /// Configuration data for the server
  40. /// </summary>
  41. private IOptions<RpcServerConfiguration> serverConfig { get; }
  42. /// <summary>
  43. /// Matches the route method name and parameters to the correct method to execute
  44. /// </summary>
  45. private IRpcRequestMatcher rpcRequestMatcher { get; }
  46. private ConcurrentDictionary<Type, ObjectFactory> objectFactoryCache { get; } = new ConcurrentDictionary<Type, ObjectFactory>();
  47. private ConcurrentDictionary<Type, (List<IAuthorizeData>, bool)> classAttributeCache { get; } = new ConcurrentDictionary<Type, (List<IAuthorizeData>, bool)>();
  48. private ConcurrentDictionary<MethodInfo, (List<IAuthorizeData>, bool)> methodAttributeCache { get; } = new ConcurrentDictionary<MethodInfo, (List<IAuthorizeData>, bool)>();
  49. /// <param name="authorizationService">Service that authorizes each method for use if configured</param>
  50. /// <param name="policyProvider">Provides authorization policies for the authroziation service</param>
  51. /// <param name="logger">Optional logger for logging Rpc invocation</param>
  52. /// <param name="serverConfig">Configuration data for the server</param>
  53. /// <param name="rpcRequestMatcher">Matches the route method name and parameters to the correct method to execute</param>
  54. public DefaultRpcInvoker(IAuthorizationService authorizationService, IAuthorizationPolicyProvider policyProvider,
  55. ILogger<DefaultRpcInvoker> logger, IOptions<RpcServerConfiguration> serverConfig,
  56. IRpcRequestMatcher rpcRequestMatcher)
  57. {
  58. this.authorizationService = authorizationService;
  59. this.policyProvider = policyProvider;
  60. this.logger = logger;
  61. this.serverConfig = serverConfig;
  62. this.rpcRequestMatcher = rpcRequestMatcher;
  63. }
  64. /// <summary>
  65. /// Call the incoming Rpc requests methods and gives the appropriate respones
  66. /// </summary>
  67. /// <param name="requests">List of Rpc requests</param>
  68. /// <param name="path">Rpc path that applies to the current request</param>
  69. /// <param name="httpContext">The context of the current http request</param>
  70. /// <returns>List of Rpc responses for the requests</returns>
  71. public async Task<List<RpcResponse>> InvokeBatchRequestAsync(IList<RpcRequest> requests, RpcPath path, IRouteContext routeContext)
  72. {
  73. this.logger?.LogDebug($"Invoking '{requests.Count}' batch requests");
  74. var invokingTasks = new List<Task<RpcResponse>>();
  75. foreach (RpcRequest request in requests)
  76. {
  77. Task<RpcResponse> invokingTask = this.InvokeRequestAsync(request, path, routeContext);
  78. if (request.Id.HasValue)
  79. {
  80. //Only wait for non-notification requests
  81. invokingTasks.Add(invokingTask);
  82. }
  83. }
  84. await Task.WhenAll(invokingTasks.ToArray());
  85. List<RpcResponse> responses = invokingTasks
  86. .Select(t => t.Result)
  87. .Where(r => r != null)
  88. .ToList();
  89. this.logger?.LogDebug($"Finished '{requests.Count}' batch requests");
  90. return responses;
  91. }
  92. /// <summary>
  93. /// Call the incoming Rpc request method and gives the appropriate response
  94. /// </summary>
  95. /// <param name="request">Rpc request</param>
  96. /// <param name="path">Rpc path that applies to the current request</param>
  97. /// <param name="httpContext">The context of the current http request</param>
  98. /// <returns>An Rpc response for the request</returns>
  99. public async Task<RpcResponse> InvokeRequestAsync(RpcRequest request, RpcPath path, IRouteContext routeContext)
  100. {
  101. if (request == null)
  102. {
  103. throw new ArgumentNullException(nameof(request));
  104. }
  105. this.logger?.LogDebug($"Invoking request with id '{request.Id}'");
  106. RpcResponse rpcResponse;
  107. try
  108. {
  109. RpcMethodInfo rpcMethod = this.GetMatchingMethod(path, request, routeContext.RouteProvider, routeContext.RequestServices);
  110. bool isAuthorized = await this.IsAuthorizedAsync(rpcMethod.Method, routeContext);
  111. if (isAuthorized)
  112. {
  113. this.logger?.LogDebug($"Attempting to invoke method '{request.Method}'");
  114. object result = await this.InvokeAsync(rpcMethod, path, routeContext.RequestServices);
  115. this.logger?.LogDebug($"Finished invoking method '{request.Method}'");
  116. if (result is IRpcMethodResult)
  117. {
  118. this.logger?.LogTrace($"Result is {nameof(IRpcMethodResult)}.");
  119. rpcResponse = ((IRpcMethodResult)result).ToRpcResponse(request.Id);
  120. }
  121. else
  122. {
  123. this.logger?.LogTrace($"Result is plain object.");
  124. rpcResponse = new RpcResponse(request.Id, result, rpcMethod.Method.ReturnType);
  125. }
  126. }
  127. else
  128. {
  129. var authError = new RpcError(RpcErrorCode.InvalidRequest, "Unauthorized");
  130. rpcResponse = new RpcResponse(request.Id, authError);
  131. }
  132. }
  133. catch (Exception ex)
  134. {
  135. string errorMessage = "An Rpc error occurred while trying to invoke request.";
  136. this.logger?.LogException(ex, errorMessage);
  137. RpcError error;
  138. if (ex is RpcException rpcException)
  139. {
  140. error = rpcException.ToRpcError(this.serverConfig.Value.ShowServerExceptions);
  141. }
  142. else
  143. {
  144. error = new RpcError(RpcErrorCode.InternalError, errorMessage, ex);
  145. }
  146. rpcResponse = new RpcResponse(request.Id, error);
  147. }
  148. if (request.Id.HasValue)
  149. {
  150. this.logger?.LogDebug($"Finished request with id: {request.Id}");
  151. //Only give a response if there is an id
  152. return rpcResponse;
  153. }
  154. this.logger?.LogDebug($"Finished request with no id. Not returning a response");
  155. return null;
  156. }
  157. private async Task<bool> IsAuthorizedAsync(MethodInfo methodInfo, IRouteContext routeContext)
  158. {
  159. (List<IAuthorizeData> authorizeDataListClass, bool allowAnonymousOnClass) = this.classAttributeCache.GetOrAdd(methodInfo.DeclaringType, GetClassAttributeInfo);
  160. (List<IAuthorizeData> authorizeDataListMethod, bool allowAnonymousOnMethod) = this.methodAttributeCache.GetOrAdd(methodInfo, GetMethodAttributeInfo);
  161. if (authorizeDataListClass.Any() || authorizeDataListMethod.Any())
  162. {
  163. if (allowAnonymousOnClass || allowAnonymousOnMethod)
  164. {
  165. this.logger?.LogDebug("Skipping authorization. Allow anonymous specified for method.");
  166. }
  167. else
  168. {
  169. this.logger?.LogDebug($"Running authorization for method.");
  170. AuthorizationResult authResult = await this.CheckAuthorize(authorizeDataListClass, routeContext);
  171. if (authResult.Succeeded)
  172. {
  173. //Have to pass both controller and method authorize
  174. authResult = await this.CheckAuthorize(authorizeDataListMethod, routeContext);
  175. }
  176. if (authResult.Succeeded)
  177. {
  178. this.logger?.LogDebug($"Authorization was successful for user '{routeContext.User.Identity.Name}'.");
  179. }
  180. else
  181. {
  182. this.logger?.LogInformation($"Authorization failed for user '{routeContext.User.Identity.Name}'.");
  183. return false;
  184. }
  185. }
  186. }
  187. else
  188. {
  189. this.logger?.LogDebug("Skipping authorization. None configured for class or method.");
  190. }
  191. return true;
  192. //functions
  193. (List<IAuthorizeData> Data, bool allowAnonymous) GetClassAttributeInfo(Type type)
  194. {
  195. return GetAttributeInfo(type.GetCustomAttributes());
  196. }
  197. (List<IAuthorizeData> Data, bool allowAnonymous) GetMethodAttributeInfo(MethodInfo info)
  198. {
  199. return GetAttributeInfo(info.GetCustomAttributes());
  200. }
  201. (List<IAuthorizeData> Data, bool allowAnonymous) GetAttributeInfo(IEnumerable<Attribute> attributes)
  202. {
  203. bool allowAnonymous = false;
  204. var dataList = new List<IAuthorizeData>(10);
  205. foreach (Attribute attribute in attributes)
  206. {
  207. if (attribute is IAuthorizeData data)
  208. {
  209. dataList.Add(data);
  210. }
  211. if (!allowAnonymous && attribute is IAllowAnonymous)
  212. {
  213. allowAnonymous = true;
  214. }
  215. }
  216. return (dataList, allowAnonymous);
  217. }
  218. }
  219. private async Task<AuthorizationResult> CheckAuthorize(List<IAuthorizeData> authorizeDataList, IRouteContext routeContext)
  220. {
  221. if (!authorizeDataList.Any())
  222. {
  223. return AuthorizationResult.Success();
  224. }
  225. AuthorizationPolicy policy = await AuthorizationPolicy.CombineAsync(this.policyProvider, authorizeDataList);
  226. return await this.authorizationService.AuthorizeAsync(routeContext.User, policy);
  227. }
  228. /// <summary>
  229. /// Finds the matching Rpc method for the current request
  230. /// </summary>
  231. /// <param name="path">Rpc route for the current request</param>
  232. /// <param name="request">Current Rpc request</param>
  233. /// <param name="parameterList">Parameter list parsed from the request</param>
  234. /// <param name="serviceProvider">(Optional)IoC Container for rpc method controllers</param>
  235. /// <returns>The matching Rpc method to the current request</returns>
  236. private RpcMethodInfo GetMatchingMethod(RpcPath path, RpcRequest request, IRpcRouteProvider routeProvider, IServiceProvider serviceProvider)
  237. {
  238. if (request == null)
  239. {
  240. throw new ArgumentNullException(nameof(request));
  241. }
  242. this.logger?.LogDebug($"Attempting to match Rpc request to a method '{request.Method}'");
  243. List<MethodInfo> allMethods = this.GetRpcMethods(path, routeProvider);
  244. List<RpcMethodInfo> matches = this.rpcRequestMatcher.FilterAndBuildMethodInfoByRequest(allMethods, request);
  245. RpcMethodInfo rpcMethod;
  246. if (matches.Count > 1)
  247. {
  248. var methodInfoList = new List<string>();
  249. foreach (RpcMethodInfo matchedMethod in matches)
  250. {
  251. var parameterTypeList = new List<string>();
  252. foreach (ParameterInfo parameterInfo in matchedMethod.Method.GetParameters())
  253. {
  254. string parameterType = parameterInfo.Name + ": " + parameterInfo.ParameterType.Name;
  255. if (parameterInfo.IsOptional)
  256. {
  257. parameterType += "(Optional)";
  258. }
  259. parameterTypeList.Add(parameterType);
  260. }
  261. string parameterString = string.Join(", ", parameterTypeList);
  262. methodInfoList.Add($"{{Name: '{matchedMethod.Method.Name}', Parameters: [{parameterString}]}}");
  263. }
  264. string errorMessage = "More than one method matched the rpc request. Unable to invoke due to ambiguity. Methods that matched the same name: " + string.Join(", ", methodInfoList);
  265. this.logger?.LogError(errorMessage);
  266. throw new RpcException(RpcErrorCode.MethodNotFound, errorMessage);
  267. }
  268. else if (matches.Count == 0)
  269. {
  270. //Log diagnostics
  271. string methodsString = string.Join(", ", allMethods.Select(m => m.Name));
  272. this.logger?.LogTrace("Methods in route: " + methodsString);
  273. const string errorMessage = "No methods matched request.";
  274. this.logger?.LogError(errorMessage);
  275. throw new RpcException(RpcErrorCode.MethodNotFound, errorMessage);
  276. }
  277. else
  278. {
  279. rpcMethod = matches.First();
  280. }
  281. this.logger?.LogDebug("Request was matched to a method");
  282. return rpcMethod;
  283. }
  284. /// <summary>
  285. /// Gets all the predefined Rpc methods for a Rpc route
  286. /// </summary>
  287. /// <param name="path">The route to get Rpc methods for</param>
  288. /// <param name="serviceProvider">(Optional) IoC Container for rpc method controllers</param>
  289. /// <returns>List of Rpc methods for the specified Rpc route</returns>
  290. private List<MethodInfo> GetRpcMethods(RpcPath path, IRpcRouteProvider routeProvider)
  291. {
  292. var methods = new List<MethodInfo>();
  293. foreach (IRpcMethodProvider methodProvider in routeProvider.GetMethodsByPath(path))
  294. {
  295. foreach (MethodInfo methodInfo in methodProvider.GetRouteMethods())
  296. {
  297. methods.Add(methodInfo);
  298. }
  299. }
  300. return methods;
  301. }
  302. /// <summary>
  303. /// Invokes the method with the specified parameters, returns the result of the method
  304. /// </summary>
  305. /// <exception cref="RpcInvalidParametersException">Thrown when conversion of parameters fails or when invoking the method is not compatible with the parameters</exception>
  306. /// <param name="parameters">List of parameters to invoke the method with</param>
  307. /// <returns>The result of the invoked method</returns>
  308. private async Task<object> InvokeAsync(RpcMethodInfo methodInfo, RpcPath path, IServiceProvider serviceProvider)
  309. {
  310. object obj = null;
  311. if (serviceProvider != null)
  312. {
  313. //Use service provider (if exists) to create instance
  314. ObjectFactory objectFactory = this.objectFactoryCache.GetOrAdd(methodInfo.Method.DeclaringType, (t) => ActivatorUtilities.CreateFactory(t, new Type[0]));
  315. obj = objectFactory(serviceProvider, null);
  316. }
  317. if (obj == null)
  318. {
  319. //Use reflection to create instance if service provider failed or is null
  320. obj = Activator.CreateInstance(methodInfo.Method.DeclaringType);
  321. }
  322. try
  323. {
  324. object returnObj = methodInfo.Method.Invoke(obj, methodInfo.ConvertedParameters);
  325. returnObj = await DefaultRpcInvoker.HandleAsyncResponses(returnObj);
  326. return returnObj;
  327. }
  328. catch (TargetInvocationException ex)
  329. {
  330. var routeInfo = new RpcRouteInfo(methodInfo, path, serviceProvider);
  331. //Controller error handling
  332. RpcErrorFilterAttribute errorFilter = methodInfo.Method.DeclaringType.GetTypeInfo().GetCustomAttribute<RpcErrorFilterAttribute>();
  333. if (errorFilter != null)
  334. {
  335. OnExceptionResult result = errorFilter.OnException(routeInfo, ex.InnerException);
  336. if (!result.ThrowException)
  337. {
  338. return result.ResponseObject;
  339. }
  340. if (result.ResponseObject is Exception rEx)
  341. {
  342. throw rEx;
  343. }
  344. }
  345. throw new RpcException(RpcErrorCode.InternalError, "Exception occurred from target method execution.", ex);
  346. }
  347. catch (Exception ex)
  348. {
  349. throw new RpcException(RpcErrorCode.InvalidParams, "Exception from attempting to invoke method. Possibly invalid parameters for method.", ex);
  350. }
  351. }
  352. /// <summary>
  353. /// Handles/Awaits the result object if it is a async Task
  354. /// </summary>
  355. /// <param name="returnObj">The result of a invoked method</param>
  356. /// <returns>Awaits a Task and returns its result if object is a Task, otherwise returns the same object given</returns>
  357. private static async Task<object> HandleAsyncResponses(object returnObj)
  358. {
  359. Task task = returnObj as Task;
  360. if (task == null) //Not async request
  361. {
  362. return returnObj;
  363. }
  364. try
  365. {
  366. await task;
  367. }
  368. catch (Exception ex)
  369. {
  370. throw new TargetInvocationException(ex);
  371. }
  372. PropertyInfo propertyInfo = task.GetType().GetProperty("Result");
  373. if (propertyInfo != null)
  374. {
  375. //Type of Task<T>. Wait for result then return it
  376. return propertyInfo.GetValue(returnObj);
  377. }
  378. //Just of type Task with no return result
  379. return null;
  380. }
  381. }
  382. }