/// <summary> /// Creates the required routes for the IOManager. /// </summary> /// <param name="handler">A handler to wrap around the call. Takes the output and API input parameters as input.</param> /// <returns>The list of API routes</returns> internal List <APIRoute> GetRequiredRoutes(Func <dynamic, object[], dynamic> handler = null) { // TODO: Rethink inputs // Might be audio, text, video, image, gesture, etc // Will be from a client app, probably not a user directly // Audio might output text but can output intent as well // Intent needs to be executed and response needs to be // negotiated based on the input // maybe a ll-accept header / ll-content-type? // Inputs can be sent as raw mime byte data or // as part of JSON as base64 encoded fields. // Here I sent up the processing of the input // API route that handles the data sent by the client // application var routes = new List <APIRoute>(); var route = new APIRoute { Path = "/input", Description = "Process the input", Method = HttpMethod.Post, Handler = Handle, RequiresAuthentication = true }; routes.Add(route); return(routes); }
public void TestRouteMatching() { var testRoute = new APIRoute(HttpMethod.Get, "/test/{id}"); var match1 = testRoute.TryMatch(new APIRequest(HttpMethod.Get, "/test/1234")); Assert.IsNotNull(match1); Assert.IsTrue(match1.ContainsKey("id")); Assert.AreEqual(match1["id"], "1234"); }
/// <summary> /// Adds a route to the API. Prepends '/api' to the path. /// </summary> /// <param name="route">The route to add</param> /// <returns>true if added, false otherwise</returns> public bool AddRoute(APIRoute route) { // Prepending '/api' to the path to ensure API routes // don't clash with static routes route.Path = $"/api{route.Path}"; if (_routes.Any(loadedRoute => loadedRoute.Path == route.Path && loadedRoute.Method == route.Method)) { return(false); } _routes.Add(route); return(true); }
/// <summary> /// Creates the required routes for all IIdentityProvider implementations. /// </summary> /// <param name="identityProvider">The provider being extended</param> /// <param name="handler">A handler to wrap around the call. Takes the output and API input parameters as input.</param> /// <returns>The list of API routes</returns> public static List <APIRoute> GetRequiredAPIRoutes(this IIdentityProvider identityProvider, Func <dynamic, object[], dynamic> handler = null) { var routes = new List <APIRoute>(); // For the IIdentityProvider interface we need to add routes to the API var route = new APIRoute { Path = "/login", Description = "Log a user in", Method = HttpMethod.Post, Handler = (APIRequest request) => { // RequiredFields property is only implemented on the APIRouteAttribute // so I have to manually do the checks for required interface routes if (request.Data.username == null || request.Data.password == null) { throw new MissingFieldException("Username and password must not be null"); } // Try..Catch as I'm calling user code here var apiResponse = new APIResponse(); var loginResult = identityProvider.Login((string)request.Data.username, (string)request.Data.password); if (loginResult.IsAuthenticated == false) { apiResponse.StatusCode = (int)HttpStatusCode.Unauthorized; apiResponse.StatusMessage = "Authentication required"; apiResponse.Data = loginResult.ErrorResponse; } else { apiResponse.Data = loginResult.User; } if (handler == null) { return(apiResponse); } // TODO: the analysis module hook needs to be implemented in a better way // Hiding the password request.Data.password = "******"; return(handler(apiResponse, new object[] { request })); } }; routes.Add(route); return(routes); }
/// <summary> /// Constructs the handler function for the route options. /// </summary> /// <param name="route">The route to build the OPTIONS for</param> /// <returns>the OPTIONS response</returns> private Func <dynamic, dynamic> BuildRouteOptions(APIRoute route) { return((dynamic parameters) => { var negotiator = Negotiate.WithStatusCode(200); negotiator.WithHeader("Access-Control-Allow-Origin", CoreContainer.Instance.Settings.Core.API.CORS.AllowedOrigin); // This fetches all the HTTP methods available to the routes // that match route.Path and adds it to the response headers string methods = string.Join(",", CoreContainer.Instance.RouteManager.GetRoutes() .Where(x => x.Path == route.Path) .Select(x => Enum.GetName(typeof(HttpMethod), x.Method).ToUpper())); negotiator.WithHeader("Access", methods); negotiator.WithHeader("Access-Control-Allow-Methods", methods); // Let's allow all the request headers for now var headers = new List <string>(Request.Headers.Keys); if (Request.Headers.Keys.Contains("Access-Control-Request-Headers")) { string acrh = Request.Headers["Access-Control-Request-Headers"].First(); if (acrh != null) { headers.AddRange(acrh.Split(',')); } } // and add some ones we know we need headers.Add("Accept-Language"); headers.Add("Request-Language"); headers.Add("Content-Type"); negotiator.WithHeader("Access-Control-Allow-Headers", string.Join(",", headers)); // Always allow credentials negotiator.WithHeader("Access-Control-Allow-Credentials", "true"); // If the route requires auth, then OPTIONS // should require it to if (route.RequiresAuthentication) { this.RequiresAuthentication(); } return negotiator; }); }
/// <summary> /// Gets all the methods marked with <see cref="APIRouteAttribute"/> in the given module /// and returns the usable API routes for the module. /// </summary> /// <param name="module">The module being extended</param> /// <param name="handler">A handler to wrap around the module invoke. Takes the output and API input parameters as input.</param> /// <returns>The list of API routes</returns> public static List <APIRoute> GetAPIRoutes(this IModule module, Func <dynamic, object[], dynamic> handler = null) { var apiRoutes = new List <APIRoute>(); // Check for APIRouteAttributes and add the routes that extend the API var methods = module.GetType().GetMethods() .Where(m => m.GetCustomAttributes(typeof(APIRouteAttribute), false).Length > 0) .ToArray(); foreach (MethodInfo methodInfo in methods) { var attributes = Attribute.GetCustomAttribute(methodInfo, typeof(APIRouteAttribute)) as APIRouteAttribute; var route = new APIRoute { Path = attributes.Path, Method = attributes.Method, Description = attributes.Description, RequiresAuthentication = attributes.RequiresAuthentication }; route.Handler = (APIRequest request) => { // For Get and Delete I'll only send the parameters // For Post and Put I'll send parameters and postData to the method // For authenticated routes I'll add the user object to the method var invokeParameters = new List <object> { request }; if (route.Method == HttpMethod.Post || route.Method == HttpMethod.Put) { // Check if postData contains required fields if (attributes.RequiredFields.Length > 0) { // There are required fields, but postData is null if (request.Data == null) { throw new NullReferenceException("POST data is required for this route"); } if (request.Headers.ContentType == MimeType.Json) { IDictionary <string, JToken> lookup = request.Data; foreach (string requiredField in attributes.RequiredFields) { if (lookup.ContainsKey(requiredField) == false) { throw new MissingFieldException($"Required data field '{requiredField}' was not found in the POST data"); } } } } } dynamic result; // TODO: the analysis module hook needs to be implemented in a better way if (handler != null) { // TODO: Ensure we can chain multiple methods result = handler(methodInfo.Invoke(module, invokeParameters.ToArray()), invokeParameters.ToArray()); } else { result = (dynamic)methodInfo.Invoke(module, invokeParameters.ToArray()); } return(result); }; apiRoutes.Add(route); } return(apiRoutes); }
public RequestContext(APIRoute route, object contextValue = null) { this.APIRoute = route; this.ContextValue = contextValue; }
/// <summary> /// Constructs the handler function including authentication. /// </summary> /// <param name="route">The route to build</param> /// <returns>The constructed function</returns> private Func <dynamic, dynamic> ComposeFunction(APIRoute route) { return((dynamic parameters) => { var request = new APIRequest(); if (route.RequiresAuthentication) { this.RequiresAuthentication(); request.AuthenticatedUser = ((InternalUserIdentity)Context.CurrentUser).Meta; } // Parse the post data, if it's JSON, deserialize into a dynamic dynamic postData = Request.Headers.ContentType == MimeType.Json ? JsonConvert.DeserializeObject(Request.Body.AsString()) : Request.Body.AsString(); // Build the request for API use request.Data = postData; request.Parameters = parameters; request.Headers = CloneHeaders(Request.Headers); var negotiator = Negotiate.WithStatusCode(200); try { var handlerResponse = route.Handler(request); if (handlerResponse is APIResponse) { var apiResponse = handlerResponse as APIResponse; negotiator.WithStatusCode(apiResponse.StatusCode); if (apiResponse.Data != null) { // TODO: Figure out how to have the negotiator work with // any content type returned by the input/output pipelines if (apiResponse.Data is string) { negotiator.WithModel(new Nancy.Responses.TextResponse((string)apiResponse.Data)); } else { negotiator.WithModel((object)apiResponse.Data); } } if (apiResponse.StatusMessage != null) { negotiator.WithReasonPhrase(apiResponse.StatusMessage); } if (apiResponse.Headers.Count > 0) { negotiator.WithHeaders(apiResponse.Headers.ToArray()); } } else { if (handlerResponse != null) { negotiator.WithModel((object)handlerResponse); } } } catch (Exception ex) { dynamic exceptionResponse = new ExpandoObject(); exceptionResponse.Type = ex.GetType(); exceptionResponse.Message = ex.Message; exceptionResponse.StackTrace = ex.StackTrace; exceptionResponse.Target = $"{ex.Source}.{ex.TargetSite.Name}"; if (ex.InnerException != null) { exceptionResponse.InnerException = ex.InnerException.Message; } // TODO: Fix this part - it breaks the whole point of negotiation. // However, dynamic doesn't work in XML anyway // Perhaps drop negiotiator and just use JSON string exception = JsonConvert.SerializeObject((object)exceptionResponse); negotiator .WithModel(new Nancy.Responses.TextResponse(exception)) .WithStatusCode(500) .WithContentType(MimeType.Json); } return negotiator; }); }
/// <summary> /// Creates the required routes for all IInteractionEngine implementations. /// </summary> /// <param name="interactionEngine">The <see cref="IInteractionEngine"/> being extended</param> /// <param name="handler">A handler to wrap around the call. Takes the output and API input parameters as input.</param> /// <returns>The list of API routes</returns> public static List <APIRoute> GetRequiredAPIRoutes(this IInteractionEngine interactionEngine, Func <dynamic, object[], dynamic> handler = null) { var routes = new List <APIRoute>(); // Create the Skills endpoints var route = new APIRoute { Path = "/skills", Description = "List available skills", Method = HttpMethod.Get, RequiresAuthentication = true, Handler = (APIRequest request) => { var response = new APIResponse(interactionEngine.ListSkills()); // TODO: the analysis module hook needs to be implemented in a better way return(handler != null?handler(response, new object[] { request }) : response); } }; routes.Add(route); route = new APIRoute { Path = "/skills", Description = "Register a new skill", Method = HttpMethod.Post, RequiresAuthentication = true, Handler = (APIRequest request) => { Skill skill = ((JObject)request.Data).ToObject <Skill>(); switch (skill.Binding) { /* * // TODO: Binary executors can't be loaded over the network using the API * // since they need a method handler * case SkillExecutorBinding.Builtin: * case SkillExecutorBinding.Module: * skill.Executor = ((JObject)skill.Executor).ToObject<BinaryExecutor>(); * break; */ case SkillExecutorBinding.Network: skill.Executor = ((JObject)skill.Executor).ToObject <NetworkExecutor>(); break; default: throw new NotImplementedException( $"The given skill binding '{skill.Binding}' is not implemented"); } APIResponse response = new APIResponse(); if (interactionEngine.RegisterSkill(skill)) { response.StatusCode = (int)HttpStatusCode.Created; response.Data = new { UUID = skill.UUID, Registered = true }; } else { response.StatusCode = (int)HttpStatusCode.Conflict; response.Data = new { Registered = false, Reason = $"The skill '{skill.UUID}' has already been registered" }; } // TODO: the analysis module hook needs to be implemented in a better way return(handler != null?handler(response, new object[] { request }) : response); } }; routes.Add(route); route = new APIRoute { Path = "/skills/{skillUUID}", Description = "Deregister a new skill", Method = HttpMethod.Delete, RequiresAuthentication = true, Handler = (APIRequest request) => { APIResponse response = new APIResponse(); if (interactionEngine.DeregisterSkill(request.Parameters.skillUUID)) { response.StatusCode = (int)HttpStatusCode.OK; } else { response.StatusCode = (int)HttpStatusCode.NotFound; response.StatusMessage = $"The skill '{request.Parameters.skillUUID}' was not found or has already been deregistered"; } // TODO: the analysis module hook needs to be implemented in a better way return(handler != null?handler(response, new object[] { request }) : response); } }; routes.Add(route); return(routes); }