2022-11-13 11:45:34 +00:00
|
|
|
|
using System.Text.Json.Serialization;
|
2022-05-26 10:47:31 +00:00
|
|
|
|
using Elsa.Expressions.Models;
|
2022-12-31 15:16:43 +00:00
|
|
|
|
using Elsa.Extensions;
|
2023-03-03 21:47:23 +00:00
|
|
|
|
using Elsa.Http.Contracts;
|
2022-05-26 10:47:31 +00:00
|
|
|
|
using Elsa.Http.Models;
|
2023-03-03 13:48:05 +00:00
|
|
|
|
using Elsa.Workflows.Core;
|
2022-05-26 10:47:31 +00:00
|
|
|
|
using Elsa.Workflows.Core.Attributes;
|
|
|
|
|
|
using Elsa.Workflows.Core.Models;
|
2023-01-16 18:42:45 +00:00
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
|
using Microsoft.AspNetCore.Routing;
|
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2023-03-07 21:02:46 +00:00
|
|
|
|
using Microsoft.Extensions.Primitives;
|
2022-01-04 08:42:12 +00:00
|
|
|
|
|
2022-05-26 10:47:31 +00:00
|
|
|
|
namespace Elsa.Http;
|
2022-01-04 08:42:12 +00:00
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Wait for an inbound HTTP request that matches the specified path and methods.
|
|
|
|
|
|
/// </summary>
|
2023-05-08 20:38:59 +00:00
|
|
|
|
[Activity("Elsa", "HTTP", "Wait for an inbound HTTP request that matches the specified path and methods.", DisplayName = "HTTP Endpoint")]
|
2023-01-16 18:42:45 +00:00
|
|
|
|
public class HttpEndpoint : Trigger<HttpRequest>
|
2022-01-04 08:42:12 +00:00
|
|
|
|
{
|
2023-01-16 18:42:45 +00:00
|
|
|
|
internal const string HttpContextInputKey = "HttpContext";
|
|
|
|
|
|
internal const string RequestPathInputKey = "RequestPath";
|
2022-03-09 23:19:00 +00:00
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
/// <inheritdoc />
|
2022-04-07 10:51:08 +00:00
|
|
|
|
[JsonConstructor]
|
|
|
|
|
|
public HttpEndpoint()
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// The path to associate with the workflow.
|
|
|
|
|
|
/// </summary>
|
2023-02-15 11:09:45 +00:00
|
|
|
|
[Input(Description = "The path to associate with the workflow.")]
|
2023-01-16 18:42:45 +00:00
|
|
|
|
public Input<string> Path { get; set; } = default!;
|
2022-01-04 08:42:12 +00:00
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// The HTTP methods to accept.
|
|
|
|
|
|
/// </summary>
|
2022-01-04 08:42:12 +00:00
|
|
|
|
[Input(
|
2023-01-16 18:42:45 +00:00
|
|
|
|
Description = "The HTTP methods to accept.",
|
2022-04-04 14:00:10 +00:00
|
|
|
|
Options = new[] { "GET", "POST", "PUT", "HEAD", "DELETE" },
|
2023-01-16 18:42:45 +00:00
|
|
|
|
UIHint = InputUIHints.CheckList)]
|
2023-03-17 20:11:55 +00:00
|
|
|
|
public Input<ICollection<string>> SupportedMethods { get; set; } = new(ObjectLiteral.From(new[] { HttpMethods.Get }));
|
2022-01-04 08:42:12 +00:00
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Allow authenticated requests only.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Input(Description = "Allow authenticated requests only.", Category = "Security")]
|
2022-03-09 23:19:00 +00:00
|
|
|
|
public Input<bool> Authorize { get; set; } = new(false);
|
|
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Provide a policy to evaluate. If the policy fails, the request is forbidden.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Input(Description = "Provide a policy to evaluate. If the policy fails, the request is forbidden.", Category = "Security")]
|
2022-03-09 23:19:00 +00:00
|
|
|
|
public Input<string?> Policy { get; set; } = new(default(string?));
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// The parsed request content, if any.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Output(Description = "The parsed request content, if any.")]
|
|
|
|
|
|
public Output<object?> ParsedContent { get; set; } = default!;
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// The parsed route data, if any.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Output(Description = "The parsed route data, if any.")]
|
2023-03-07 21:02:46 +00:00
|
|
|
|
public Output<IDictionary<string, object>> RouteData { get; set; } = default!;
|
2023-03-07 13:44:34 +00:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// The querystring data, if any.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Output(Description = "The querystring data, if any.")]
|
2023-03-07 21:02:46 +00:00
|
|
|
|
public Output<IDictionary<string, object>> QueryStringData { get; set; } = default!;
|
2023-03-07 13:44:34 +00:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// The headers, if any.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[Output(Description = "The headers, if any.")]
|
2023-03-07 21:02:46 +00:00
|
|
|
|
public Output<IDictionary<string, object>> Headers { get; set; } = default!;
|
2022-01-04 08:42:12 +00:00
|
|
|
|
|
2022-11-23 10:20:00 +00:00
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
protected override IEnumerable<object> GetTriggerPayloads(TriggerIndexingContext context) => GetBookmarkPayloads(context.ExpressionExecutionContext);
|
2022-03-09 23:19:00 +00:00
|
|
|
|
|
2022-11-23 10:20:00 +00:00
|
|
|
|
/// <inheritdoc />
|
|
|
|
|
|
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
2022-03-07 14:08:01 +00:00
|
|
|
|
{
|
2023-06-28 18:55:57 +00:00
|
|
|
|
if (!context.IsTriggerOfWorkflow())
|
2022-03-07 14:08:01 +00:00
|
|
|
|
{
|
2022-11-23 10:20:00 +00:00
|
|
|
|
context.CreateBookmarks(GetBookmarkPayloads(context.ExpressionExecutionContext));
|
2022-03-07 14:08:01 +00:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-23 22:30:00 +00:00
|
|
|
|
var httpContextAccessor = context.GetRequiredService<IHttpContextAccessor>();
|
|
|
|
|
|
var httpContext = httpContextAccessor.HttpContext;
|
|
|
|
|
|
|
|
|
|
|
|
if (httpContext == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// We're executing in a non-HTTP context (e.g. in a virtual actor).
|
|
|
|
|
|
// Create a bookmark to allow the invoker to export the state and resume execution from there.
|
|
|
|
|
|
context.CreateBookmark(OnResumeAsync);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await HandleRequestAsync(context, httpContext);
|
2022-03-07 14:08:01 +00:00
|
|
|
|
}
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-23 22:30:00 +00:00
|
|
|
|
private async ValueTask OnResumeAsync(ActivityExecutionContext context)
|
|
|
|
|
|
{
|
|
|
|
|
|
var httpContextAccessor = context.GetRequiredService<IHttpContextAccessor>();
|
|
|
|
|
|
var httpContext = httpContextAccessor.HttpContext;
|
|
|
|
|
|
|
|
|
|
|
|
if (httpContext == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// We're not in an HTTP context, so let's fail.
|
|
|
|
|
|
throw new Exception("Cannot execute in a non-HTTP context");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await HandleRequestAsync(context, httpContext);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
private async Task<object?> ParseContentAsync(ActivityExecutionContext context, HttpRequest httpRequest)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!HasContent(httpRequest))
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
|
|
var cancellationToken = context.CancellationToken;
|
|
|
|
|
|
var targetType = ParsedContent.GetTargetType(context);
|
|
|
|
|
|
var contentStream = httpRequest.Body;
|
2023-05-28 16:35:45 +00:00
|
|
|
|
var contentType = httpRequest.ContentType!;
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
return await context.ParseContentAsync(contentStream, contentType, targetType, cancellationToken);
|
|
|
|
|
|
}
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
private static bool HasContent(HttpRequest httpRequest) => httpRequest.Headers.ContentLength > 0;
|
2022-01-04 08:42:12 +00:00
|
|
|
|
|
2022-11-23 10:20:00 +00:00
|
|
|
|
private IEnumerable<object> GetBookmarkPayloads(ExpressionExecutionContext context)
|
2022-01-04 08:42:12 +00:00
|
|
|
|
{
|
2022-03-07 11:01:27 +00:00
|
|
|
|
// Generate bookmark data for path and selected methods.
|
2022-01-13 21:38:01 +00:00
|
|
|
|
var path = context.Get(Path);
|
2023-05-28 16:35:45 +00:00
|
|
|
|
var methods = SupportedMethods.GetOrDefault(context) ?? new List<string>{ HttpMethods.Get };
|
|
|
|
|
|
var authorize = Authorize.GetOrDefault(context);
|
|
|
|
|
|
var policy = Policy.GetOrDefault(context);
|
2023-03-08 08:49:43 +00:00
|
|
|
|
return methods!.Select(x =>
|
|
|
|
|
|
new HttpEndpointBookmarkPayload(path!, x.ToLowerInvariant(), authorize, policy))
|
|
|
|
|
|
.Cast<object>().ToArray();
|
2022-01-04 08:42:12 +00:00
|
|
|
|
}
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-23 22:30:00 +00:00
|
|
|
|
private async Task HandleRequestAsync(ActivityExecutionContext context, HttpContext httpContext)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Provide the received HTTP request as output.
|
|
|
|
|
|
var request = httpContext.Request;
|
|
|
|
|
|
context.Set(Result, request);
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-23 22:30:00 +00:00
|
|
|
|
// Read route data, if any.
|
|
|
|
|
|
var path = context.GetInput<PathString>(RequestPathInputKey);
|
|
|
|
|
|
var routeData = GetRouteData(httpContext, path);
|
2023-03-07 13:44:34 +00:00
|
|
|
|
|
2023-05-28 16:35:45 +00:00
|
|
|
|
var routeDictionary = routeData.Values.ToDictionary(route => route.Key, route => route.Value!);
|
2023-03-07 21:02:46 +00:00
|
|
|
|
var queryStringDictionary = httpContext.Request.Query.ToDictionary<KeyValuePair<string, StringValues>, string, object>(queryString => queryString.Key, queryString => queryString.Value[0]!);
|
|
|
|
|
|
var headersDictionary = httpContext.Request.Headers.ToDictionary<KeyValuePair<string, StringValues>, string, object>(header => header.Key, header => header.Value[0]!);
|
2023-03-07 13:44:34 +00:00
|
|
|
|
|
|
|
|
|
|
context.Set(RouteData, routeDictionary);
|
|
|
|
|
|
context.Set(QueryStringData, queryStringDictionary);
|
|
|
|
|
|
context.Set(Headers, headersDictionary);
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-23 22:30:00 +00:00
|
|
|
|
// Read content, if any.
|
|
|
|
|
|
var content = await ParseContentAsync(context, request);
|
|
|
|
|
|
context.Set(ParsedContent, content);
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-23 22:30:00 +00:00
|
|
|
|
// Complete.
|
|
|
|
|
|
await context.CompleteActivityAsync();
|
|
|
|
|
|
}
|
2023-02-15 11:09:45 +00:00
|
|
|
|
|
2023-01-16 18:42:45 +00:00
|
|
|
|
private static RouteData GetRouteData(HttpContext httpContext, string path)
|
|
|
|
|
|
{
|
|
|
|
|
|
var routeData = httpContext.GetRouteData();
|
|
|
|
|
|
var routeTable = httpContext.RequestServices.GetRequiredService<IRouteTable>();
|
|
|
|
|
|
var routeMatcher = httpContext.RequestServices.GetRequiredService<IRouteMatcher>();
|
|
|
|
|
|
|
|
|
|
|
|
var matchingRouteQuery =
|
|
|
|
|
|
from route in routeTable
|
|
|
|
|
|
let routeValues = routeMatcher.Match(route, path)
|
|
|
|
|
|
where routeValues != null
|
|
|
|
|
|
select new { route, routeValues };
|
|
|
|
|
|
|
|
|
|
|
|
var matchingRoute = matchingRouteQuery.FirstOrDefault();
|
|
|
|
|
|
|
|
|
|
|
|
if (matchingRoute == null)
|
|
|
|
|
|
return routeData;
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var (key, value) in matchingRoute.routeValues!)
|
|
|
|
|
|
routeData.Values[key] = value;
|
|
|
|
|
|
|
|
|
|
|
|
return routeData;
|
|
|
|
|
|
}
|
2022-01-04 08:42:12 +00:00
|
|
|
|
}
|