/*
         * Default JSON handler, simply returning a JsonResult to caller.
         */
        internal static IActionResult JsonHandler(MagicResponse response)
        {
            // Checking if JSON is already converted into a string, at which point we return it as such.
            if (response.Content is string strContent)
            {
                return new ContentResult {
                           Content = strContent, StatusCode = response.Result
                }
            }
            ;

            // Strongly typed JSON object, hence returning as such.
            return(new JsonResult(response.Content as JToken)
            {
                StatusCode = response.Result
            });
        }
    }
Example #2
0
        /*
         * Transforms from our internal HttpResponse wrapper to an ActionResult
         */
        IActionResult HandleResponse(MagicResponse response)
        {
            // Decorating envelope of response which returns Content-Type to us.
            var contentType = DecorateResponseEnvelope(response);

            // If empty result, we return nothing.
            if (response.Content == null)
            {
                return(new StatusCodeResult(response.Result));
            }

            /*
             * Figuring out how to return response, which depends upon its Content-Type, and
             * whether or not we have a registered handler for specified Content-Type or not.
             */
            if (_responseHandlers.TryGetValue(contentType, out var functor))
            {
                return(functor(response));
            }
            else
            {
                // Generic handler for everything except specialised handlers registered for different content types.
                if (response.Content is string strResponse)
                {
                    return new ContentResult {
                               Content = strResponse, StatusCode = response.Result
                    }
                }
                ;

                if (response.Content is byte[] bytesResponse)
                {
                    return(new FileContentResult(bytesResponse, Response.ContentType));
                }

                if (response.Content is Stream streamResponse)
                {
                    return(new FileStreamResult(streamResponse, Response.ContentType));
                }

                throw new HyperlambdaException($"Unsupported return value from Hyperlambda, returning objects of type '{response.Content.GetType().FullName}' is not supported");
            }
        }

        /*
         * Responsible for decorating envlope of HTTP response.
         */
        string DecorateResponseEnvelope(MagicResponse response)
        {
            // Making sure we attach any explicitly added HTTP headers to the response.
            foreach (var idx in response.Headers)
            {
                Response.Headers.Add(idx.Key, idx.Value);
            }

            // Making sure we attach all cookies.
            foreach (var idx in response.Cookies)
            {
                var options = new CookieOptions
                {
                    Secure   = idx.Secure,
                    Expires  = idx.Expires,
                    HttpOnly = idx.HttpOnly,
                    Domain   = idx.Domain,
                    Path     = idx.Path,
                };
                if (!string.IsNullOrEmpty(idx.SameSite))
                {
                    options.SameSite = (SameSiteMode)Enum.Parse(typeof(SameSiteMode), idx.SameSite, true);
                }
                Response.Cookies.Append(idx.Name, idx.Value, options);
            }

            // Unless explicitly overridden by service, we default Content-Type to JSON / UTF8.
            if (!response.Headers.ContainsKey("Content-Type") || string.IsNullOrEmpty(response.Headers["Content-Type"]))
            {
                Response.ContentType = "application/json; char-set=utf-8";
                return("application/json");
            }
            else
            {
                // Figuring out Content-Type (minus arguments).
                return(Response.ContentType
                       .Split(';')
                       .Select(x => x.Trim())
                       .FirstOrDefault()
                       .ToLowerInvariant());
            }
        }

        #endregion
    }
        /// <inheritdoc/>
        public async Task <MagicResponse> ExecuteAsync(MagicRequest request)
        {
            // Sanity checking invocation
            if (string.IsNullOrEmpty(request.URL))
            {
                return new MagicResponse {
                           Result = 404
                }
            }
            ;

            // Making sure we never resolve to anything outside of "modules/" and "system/" folder.
            if (!request.URL.StartsWith("modules/") && !request.URL.StartsWith("system/"))
            {
                return new MagicResponse {
                           Result = 401
                }
            }
            ;

            // Figuring out file to execute, and doing some basic sanity checking.
            var path = Utilities.GetEndpointFilePath(_rootResolver, request.URL, request.Verb);

            if (!await _fileService.ExistsAsync(path))
            {
                return new MagicResponse {
                           Result = 404
                }
            }
            ;

            // Creating our lambda object by loading Hyperlambda file.
            var lambda = HyperlambdaParser.Parse(await _fileService.LoadAsync(path));

            // Applying interceptors.
            lambda = await ApplyInterceptors(lambda, request.URL);

            // Attaching arguments.
            _argumentsHandler.Attach(lambda, request.Query, request.Payload);

            // Invoking method responsible for actually executing lambda object.
            return(await ExecuteAsync(lambda, request));
        }

        #region [ -- Private helper methods -- ]

        /*
         * Applies interceptors to specified Node/Lambda object.
         */
        async Task <Node> ApplyInterceptors(Node result, string url)
        {
            // Checking to see if interceptors exists recursively upwards in folder hierarchy.
            var splits = url.Split(new char [] { '/' }, StringSplitOptions.RemoveEmptyEntries);

            // Stripping away last entity (filename) of invocation.
            var folders = splits.Take(splits.Length - 1);

            // Iterating as long as we have more entities in list of folders.
            while (true)
            {
                // Checking if "current-folder/interceptor.hl" file exists.
                var current = _rootResolver.AbsolutePath(string.Join("/", folders) + "/interceptor.hl");

                if (_fileService.Exists(current))
                {
                    result = await ApplyInterceptor(result, current);
                }

                // Checking if we're done, and at root folder, at which point we break while loop.
                if (!folders.Any())
                {
                    break; // We're done, no more interceptors!
                }
                // Traversing upwards in hierarchy to be able to nest interceptors upwards in hierarchy.
                folders = folders.Take(folders.Count() - 1);
            }

            // Returning result to caller.
            return(result);
        }

        /*
         * Applies the specified interceptor and returns the transformed Node/Lambda result.
         */
        async Task <Node> ApplyInterceptor(Node lambda, string interceptorFile)
        {
            // Getting interceptor lambda.
            var interceptNode = HyperlambdaParser.Parse(await _fileService.LoadAsync(interceptorFile));

            // Moving [.arguments] from endpoint lambda to the top of interceptor lambda if existing.
            var args = lambda
                       .Children
                       .Where(x =>
                              x.Name == ".arguments" ||
                              x.Name == ".description" ||
                              x.Name == ".type" ||
                              x.Name == "auth.ticket.verify" ||
                              x.Name.StartsWith("validators."));

            // Notice, reversing arguments nodes makes sure we apply arguments in order of appearance.
            foreach (var idx in args.Reverse().ToList())
            {
                interceptNode.Insert(0, idx); // Notice, will detach the argument from its original position!
            }

            // Moving endpoint Lambda to position before any [.interceptor] node found in interceptor lambda.
            foreach (var idxLambda in new Expression("**/.interceptor").Evaluate(interceptNode).ToList())
            {
                // Iterating through each node in current result and injecting before currently iterated [.lambda] node.
                foreach (var idx in lambda.Children)
                {
                    // This logic ensures we keep existing order without any fuzz.
                    // By cloning node we also support having multiple [.interceptor] nodes.
                    idxLambda.InsertBefore(idx.Clone());
                }

                // Removing currently iterated [.interceptor] node in interceptor lambda object.
                idxLambda.Parent.Remove(idxLambda);
            }

            // Returning interceptor Node/Lambda which is now the root of the execution Lambda object.
            return(interceptNode);
        }

        /*
         * Method responsible for actually executing lambda object after file has been loaded,
         * interceptors and arguments have been applied, and transforming result of invocation
         * to a MagicResponse.
         */
        async Task <MagicResponse> ExecuteAsync(Node lambda, MagicRequest request)
        {
            // Creating our result wrapper, wrapping whatever the endpoint wants to return to the client.
            var result   = new Node();
            var response = new MagicResponse();

            try
            {
                await _signaler.ScopeAsync("http.request", request, async() =>
                {
                    await _signaler.ScopeAsync("http.response", response, async() =>
                    {
                        await _signaler.ScopeAsync("slots.result", result, async() =>
                        {
                            await _signaler.SignalAsync("eval", lambda);
                        });
                    });
                });

                response.Content = GetReturnValue(response, result);
                return(response);
            }
            catch
            {
                if (result.Value is IDisposable disposable)
                {
                    disposable.Dispose();
                }
                if (response.Content is IDisposable disposable2 && !object.ReferenceEquals(response.Content, result.Value))
                {
                    disposable2.Dispose();
                }
                throw;
            }
        }

        /*
         * Creates a returned payload of some sort and returning to caller.
         */
        object GetReturnValue(MagicResponse httpResponse, Node lambda)
        {
            /*
             * An endpoint can return either a Node/Lambda hierarchy or a simple value.
             * First we check if endpoint returned a simple value, at which point we convert it to
             * a string. Notice, we're prioritising simple values, implying if return node has a
             * simple value, none of its children nodes will be returned.
             */
            if (lambda.Value != null)
            {
                // IDisposables (Streams e.g.) are automatically disposed by ASP.NET Core.
                if (lambda.Value is IDisposable || lambda.Value is byte[])
                {
                    return(lambda.Value);
                }

                return(lambda.Get <string>());
            }
            else if (lambda.Children.Any())
            {
                // Checking if we should return content as Hyperlambda.
                if (httpResponse.Headers.TryGetValue("Content-Type", out var val) && val == "application/x-hyperlambda")
                {
                    return(HyperlambdaGenerator.GetHyperlambda(lambda.Children));
                }

                // Defaulting to returning content as JSON by converting from Lambda to JSON.
                var convert = new Node();
                convert.AddRange(lambda.Children.ToList());
                _signaler.Signal(".lambda2json-raw", convert);
                return(convert.Value);
            }
            return(null); // No content
        }

        #endregion
    }
}