123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- using System;
- using System.Collections.Generic;
- using System.Globalization;
- using System.Linq;
- using System.Reflection;
- using System.Threading.Tasks;
- using EdjCase.JsonRpc.Core;
- using EdjCase.JsonRpc.Router.Abstractions;
- using Microsoft.Extensions.Logging;
- using Newtonsoft.Json;
- using Newtonsoft.Json.Linq;
- using EdjCase.JsonRpc.Router.Utilities;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Http;
- using Microsoft.Extensions.Options;
- using Microsoft.Extensions.DependencyInjection;
- using EdjCase.JsonRpc.Router.MethodProviders;
- using System.Collections.Concurrent;
- namespace EdjCase.JsonRpc.Router.Defaults
- {
- /// <summary>
- /// Default Rpc method invoker that uses asynchronous processing
- /// </summary>
- public class DefaultRpcInvoker : IRpcInvoker
- {
- /// <summary>
- /// Logger for logging Rpc invocation
- /// </summary>
- private ILogger<DefaultRpcInvoker> logger { get; }
- /// <summary>
- /// AspNet service to authorize requests
- /// </summary>
- private IAuthorizationService authorizationService { get; }
- /// <summary>
- /// Provides authorization policies for the authroziation service
- /// </summary>
- private IAuthorizationPolicyProvider policyProvider { get; }
- /// <summary>
- /// Configuration data for the server
- /// </summary>
- private IOptions<RpcServerConfiguration> serverConfig { get; }
- /// <summary>
- /// Matches the route method name and parameters to the correct method to execute
- /// </summary>
- private IRpcRequestMatcher rpcRequestMatcher { get; }
- private ConcurrentDictionary<Type, ObjectFactory> objectFactoryCache { get; } = new ConcurrentDictionary<Type, ObjectFactory>();
- private ConcurrentDictionary<Type, (List<IAuthorizeData>, bool)> classAttributeCache { get; } = new ConcurrentDictionary<Type, (List<IAuthorizeData>, bool)>();
- private ConcurrentDictionary<MethodInfo, (List<IAuthorizeData>, bool)> methodAttributeCache { get; } = new ConcurrentDictionary<MethodInfo, (List<IAuthorizeData>, bool)>();
- /// <param name="authorizationService">Service that authorizes each method for use if configured</param>
- /// <param name="policyProvider">Provides authorization policies for the authroziation service</param>
- /// <param name="logger">Optional logger for logging Rpc invocation</param>
- /// <param name="serverConfig">Configuration data for the server</param>
- /// <param name="rpcRequestMatcher">Matches the route method name and parameters to the correct method to execute</param>
- public DefaultRpcInvoker(IAuthorizationService authorizationService, IAuthorizationPolicyProvider policyProvider,
- ILogger<DefaultRpcInvoker> logger, IOptions<RpcServerConfiguration> serverConfig,
- IRpcRequestMatcher rpcRequestMatcher)
- {
- this.authorizationService = authorizationService;
- this.policyProvider = policyProvider;
- this.logger = logger;
- this.serverConfig = serverConfig;
- this.rpcRequestMatcher = rpcRequestMatcher;
- }
- /// <summary>
- /// Call the incoming Rpc requests methods and gives the appropriate respones
- /// </summary>
- /// <param name="requests">List of Rpc requests</param>
- /// <param name="path">Rpc path that applies to the current request</param>
- /// <param name="httpContext">The context of the current http request</param>
- /// <returns>List of Rpc responses for the requests</returns>
- public async Task<List<RpcResponse>> InvokeBatchRequestAsync(IList<RpcRequest> requests, RpcPath path, IRouteContext routeContext)
- {
- this.logger?.LogDebug($"Invoking '{requests.Count}' batch requests");
- var invokingTasks = new List<Task<RpcResponse>>();
- foreach (RpcRequest request in requests)
- {
- Task<RpcResponse> invokingTask = this.InvokeRequestAsync(request, path, routeContext);
- if (request.Id.HasValue)
- {
- //Only wait for non-notification requests
- invokingTasks.Add(invokingTask);
- }
- }
- await Task.WhenAll(invokingTasks.ToArray());
- List<RpcResponse> responses = invokingTasks
- .Select(t => t.Result)
- .Where(r => r != null)
- .ToList();
- this.logger?.LogDebug($"Finished '{requests.Count}' batch requests");
- return responses;
- }
- /// <summary>
- /// Call the incoming Rpc request method and gives the appropriate response
- /// </summary>
- /// <param name="request">Rpc request</param>
- /// <param name="path">Rpc path that applies to the current request</param>
- /// <param name="httpContext">The context of the current http request</param>
- /// <returns>An Rpc response for the request</returns>
- public async Task<RpcResponse> InvokeRequestAsync(RpcRequest request, RpcPath path, IRouteContext routeContext)
- {
- if (request == null)
- {
- throw new ArgumentNullException(nameof(request));
- }
- this.logger?.LogDebug($"Invoking request with id '{request.Id}'");
- RpcResponse rpcResponse;
- try
- {
- RpcMethodInfo rpcMethod = this.GetMatchingMethod(path, request, routeContext.RouteProvider, routeContext.RequestServices);
- bool isAuthorized = await this.IsAuthorizedAsync(rpcMethod.Method, routeContext);
- if (isAuthorized)
- {
- this.logger?.LogDebug($"Attempting to invoke method '{request.Method}'");
- object result = await this.InvokeAsync(rpcMethod, path, routeContext.RequestServices);
- this.logger?.LogDebug($"Finished invoking method '{request.Method}'");
- if (result is IRpcMethodResult)
- {
- this.logger?.LogTrace($"Result is {nameof(IRpcMethodResult)}.");
- rpcResponse = ((IRpcMethodResult)result).ToRpcResponse(request.Id);
- }
- else
- {
- this.logger?.LogTrace($"Result is plain object.");
- rpcResponse = new RpcResponse(request.Id, result, rpcMethod.Method.ReturnType);
- }
- }
- else
- {
- var authError = new RpcError(RpcErrorCode.InvalidRequest, "Unauthorized");
- rpcResponse = new RpcResponse(request.Id, authError);
- }
- }
- catch (Exception ex)
- {
- string errorMessage = "An Rpc error occurred while trying to invoke request.";
- this.logger?.LogException(ex, errorMessage);
- RpcError error;
- if (ex is RpcException rpcException)
- {
- error = rpcException.ToRpcError(this.serverConfig.Value.ShowServerExceptions);
- }
- else
- {
- error = new RpcError(RpcErrorCode.InternalError, errorMessage, ex);
- }
- rpcResponse = new RpcResponse(request.Id, error);
- }
- if (request.Id.HasValue)
- {
- this.logger?.LogDebug($"Finished request with id: {request.Id}");
- //Only give a response if there is an id
- return rpcResponse;
- }
- this.logger?.LogDebug($"Finished request with no id. Not returning a response");
- return null;
- }
- private async Task<bool> IsAuthorizedAsync(MethodInfo methodInfo, IRouteContext routeContext)
- {
- (List<IAuthorizeData> authorizeDataListClass, bool allowAnonymousOnClass) = this.classAttributeCache.GetOrAdd(methodInfo.DeclaringType, GetClassAttributeInfo);
- (List<IAuthorizeData> authorizeDataListMethod, bool allowAnonymousOnMethod) = this.methodAttributeCache.GetOrAdd(methodInfo, GetMethodAttributeInfo);
- if (authorizeDataListClass.Any() || authorizeDataListMethod.Any())
- {
- if (allowAnonymousOnClass || allowAnonymousOnMethod)
- {
- this.logger?.LogDebug("Skipping authorization. Allow anonymous specified for method.");
- }
- else
- {
- this.logger?.LogDebug($"Running authorization for method.");
- AuthorizationResult authResult = await this.CheckAuthorize(authorizeDataListClass, routeContext);
- if (authResult.Succeeded)
- {
- //Have to pass both controller and method authorize
- authResult = await this.CheckAuthorize(authorizeDataListMethod, routeContext);
- }
- if (authResult.Succeeded)
- {
- this.logger?.LogDebug($"Authorization was successful for user '{routeContext.User.Identity.Name}'.");
- }
- else
- {
- this.logger?.LogInformation($"Authorization failed for user '{routeContext.User.Identity.Name}'.");
- return false;
- }
- }
- }
- else
- {
- this.logger?.LogDebug("Skipping authorization. None configured for class or method.");
- }
- return true;
- //functions
- (List<IAuthorizeData> Data, bool allowAnonymous) GetClassAttributeInfo(Type type)
- {
- return GetAttributeInfo(type.GetCustomAttributes());
- }
- (List<IAuthorizeData> Data, bool allowAnonymous) GetMethodAttributeInfo(MethodInfo info)
- {
- return GetAttributeInfo(info.GetCustomAttributes());
- }
- (List<IAuthorizeData> Data, bool allowAnonymous) GetAttributeInfo(IEnumerable<Attribute> attributes)
- {
- bool allowAnonymous = false;
- var dataList = new List<IAuthorizeData>(10);
- foreach (Attribute attribute in attributes)
- {
- if (attribute is IAuthorizeData data)
- {
- dataList.Add(data);
- }
- if (!allowAnonymous && attribute is IAllowAnonymous)
- {
- allowAnonymous = true;
- }
- }
- return (dataList, allowAnonymous);
- }
- }
- private async Task<AuthorizationResult> CheckAuthorize(List<IAuthorizeData> authorizeDataList, IRouteContext routeContext)
- {
- if (!authorizeDataList.Any())
- {
- return AuthorizationResult.Success();
- }
- AuthorizationPolicy policy = await AuthorizationPolicy.CombineAsync(this.policyProvider, authorizeDataList);
- return await this.authorizationService.AuthorizeAsync(routeContext.User, policy);
- }
- /// <summary>
- /// Finds the matching Rpc method for the current request
- /// </summary>
- /// <param name="path">Rpc route for the current request</param>
- /// <param name="request">Current Rpc request</param>
- /// <param name="parameterList">Parameter list parsed from the request</param>
- /// <param name="serviceProvider">(Optional)IoC Container for rpc method controllers</param>
- /// <returns>The matching Rpc method to the current request</returns>
- private RpcMethodInfo GetMatchingMethod(RpcPath path, RpcRequest request, IRpcRouteProvider routeProvider, IServiceProvider serviceProvider)
- {
- if (request == null)
- {
- throw new ArgumentNullException(nameof(request));
- }
- this.logger?.LogDebug($"Attempting to match Rpc request to a method '{request.Method}'");
- List<MethodInfo> allMethods = this.GetRpcMethods(path, routeProvider);
- List<RpcMethodInfo> matches = this.rpcRequestMatcher.FilterAndBuildMethodInfoByRequest(allMethods, request);
- RpcMethodInfo rpcMethod;
- if (matches.Count > 1)
- {
- var methodInfoList = new List<string>();
- foreach (RpcMethodInfo matchedMethod in matches)
- {
- var parameterTypeList = new List<string>();
- foreach (ParameterInfo parameterInfo in matchedMethod.Method.GetParameters())
- {
- string parameterType = parameterInfo.Name + ": " + parameterInfo.ParameterType.Name;
- if (parameterInfo.IsOptional)
- {
- parameterType += "(Optional)";
- }
- parameterTypeList.Add(parameterType);
- }
- string parameterString = string.Join(", ", parameterTypeList);
- methodInfoList.Add($"{{Name: '{matchedMethod.Method.Name}', Parameters: [{parameterString}]}}");
- }
- 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);
- this.logger?.LogError(errorMessage);
- throw new RpcException(RpcErrorCode.MethodNotFound, errorMessage);
- }
- else if (matches.Count == 0)
- {
- //Log diagnostics
- string methodsString = string.Join(", ", allMethods.Select(m => m.Name));
- this.logger?.LogTrace("Methods in route: " + methodsString);
-
- const string errorMessage = "No methods matched request.";
- this.logger?.LogError(errorMessage);
- throw new RpcException(RpcErrorCode.MethodNotFound, errorMessage);
- }
- else
- {
- rpcMethod = matches.First();
- }
- this.logger?.LogDebug("Request was matched to a method");
- return rpcMethod;
- }
- /// <summary>
- /// Gets all the predefined Rpc methods for a Rpc route
- /// </summary>
- /// <param name="path">The route to get Rpc methods for</param>
- /// <param name="serviceProvider">(Optional) IoC Container for rpc method controllers</param>
- /// <returns>List of Rpc methods for the specified Rpc route</returns>
- private List<MethodInfo> GetRpcMethods(RpcPath path, IRpcRouteProvider routeProvider)
- {
- var methods = new List<MethodInfo>();
- foreach (IRpcMethodProvider methodProvider in routeProvider.GetMethodsByPath(path))
- {
- foreach (MethodInfo methodInfo in methodProvider.GetRouteMethods())
- {
- methods.Add(methodInfo);
- }
- }
- return methods;
- }
- /// <summary>
- /// Invokes the method with the specified parameters, returns the result of the method
- /// </summary>
- /// <exception cref="RpcInvalidParametersException">Thrown when conversion of parameters fails or when invoking the method is not compatible with the parameters</exception>
- /// <param name="parameters">List of parameters to invoke the method with</param>
- /// <returns>The result of the invoked method</returns>
- private async Task<object> InvokeAsync(RpcMethodInfo methodInfo, RpcPath path, IServiceProvider serviceProvider)
- {
- object obj = null;
- if (serviceProvider != null)
- {
- //Use service provider (if exists) to create instance
- ObjectFactory objectFactory = this.objectFactoryCache.GetOrAdd(methodInfo.Method.DeclaringType, (t) => ActivatorUtilities.CreateFactory(t, new Type[0]));
- obj = objectFactory(serviceProvider, null);
- }
- if (obj == null)
- {
- //Use reflection to create instance if service provider failed or is null
- obj = Activator.CreateInstance(methodInfo.Method.DeclaringType);
- }
- try
- {
- object returnObj = methodInfo.Method.Invoke(obj, methodInfo.ConvertedParameters);
- returnObj = await DefaultRpcInvoker.HandleAsyncResponses(returnObj);
- return returnObj;
- }
- catch (TargetInvocationException ex)
- {
- var routeInfo = new RpcRouteInfo(methodInfo, path, serviceProvider);
- //Controller error handling
- RpcErrorFilterAttribute errorFilter = methodInfo.Method.DeclaringType.GetTypeInfo().GetCustomAttribute<RpcErrorFilterAttribute>();
- if (errorFilter != null)
- {
- OnExceptionResult result = errorFilter.OnException(routeInfo, ex.InnerException);
- if (!result.ThrowException)
- {
- return result.ResponseObject;
- }
- if (result.ResponseObject is Exception rEx)
- {
- throw rEx;
- }
- }
- throw new RpcException(RpcErrorCode.InternalError, "Exception occurred from target method execution.", ex);
- }
- catch (Exception ex)
- {
- throw new RpcException(RpcErrorCode.InvalidParams, "Exception from attempting to invoke method. Possibly invalid parameters for method.", ex);
- }
- }
- /// <summary>
- /// Handles/Awaits the result object if it is a async Task
- /// </summary>
- /// <param name="returnObj">The result of a invoked method</param>
- /// <returns>Awaits a Task and returns its result if object is a Task, otherwise returns the same object given</returns>
- private static async Task<object> HandleAsyncResponses(object returnObj)
- {
- Task task = returnObj as Task;
- if (task == null) //Not async request
- {
- return returnObj;
- }
- try
- {
- await task;
- }
- catch (Exception ex)
- {
- throw new TargetInvocationException(ex);
- }
- PropertyInfo propertyInfo = task.GetType().GetProperty("Result");
- if (propertyInfo != null)
- {
- //Type of Task<T>. Wait for result then return it
- return propertyInfo.GetValue(returnObj);
- }
- //Just of type Task with no return result
- return null;
- }
- }
- }
|