Normalize routes

This fixes an issue where routes are stored in an inconsistent format, potentially breaking workflow HTTP resumption due to bookmark hash mismatches.
This commit is contained in:
Sipke Schoorstra 2023-10-20 09:51:56 +02:00
parent c63369b567
commit 366ce2fabd
7 changed files with 77 additions and 24 deletions

View file

@ -1,4 +1,8 @@
using System.Runtime.CompilerServices;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Elsa.Expressions.Models;
using Elsa.Extensions;
using Elsa.Http.Bookmarks;
@ -436,7 +440,7 @@ public class HttpEndpoint : Trigger<HttpRequest>
private IEnumerable<object> GetBookmarkPayloads(ExpressionExecutionContext context)
{
// Generate bookmark data for path and selected methods.
var path = context.Get(Path);
var normalizedRoute = context.Get(Path)!.NormalizeRoute();
var methods = SupportedMethods.GetOrDefault(context) ?? new List<string> { HttpMethods.Get };
var authorize = Authorize.GetOrDefault(context);
var policy = Policy.GetOrDefault(context);
@ -444,7 +448,7 @@ public class HttpEndpoint : Trigger<HttpRequest>
var requestSizeLimit = RequestSizeLimit.GetOrDefault(context);
return methods
.Select(x => new HttpEndpointBookmarkPayload(path!, x.ToLowerInvariant(), authorize, policy, requestTimeout, requestSizeLimit))
.Select(x => new HttpEndpointBookmarkPayload(normalizedRoute, x.ToLowerInvariant(), authorize, policy, requestTimeout, requestSizeLimit))
.Cast<object>()
.ToArray();
}

View file

@ -10,5 +10,5 @@ public interface IRouteMatcher
/// <summary>
/// Matches a given request path against the specified route template.
/// </summary>
RouteValueDictionary? Match(string routeTemplate, string requestPath);
RouteValueDictionary? Match(string routeTemplate, string route);
}

View file

@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Elsa.Http.Contracts;
/// <summary>
@ -5,8 +7,27 @@ namespace Elsa.Http.Contracts;
/// </summary>
public interface IRouteTable : IEnumerable<string>
{
void Add(string path);
void Remove(string path);
void AddRange(IEnumerable<string> paths);
void RemoveRange(IEnumerable<string> paths);
/// <summary>
/// Adds a route to the table.
/// </summary>
/// <param name="route">The route to add.</param>
void Add(string route);
/// <summary>
/// Removes a route from the table.
/// </summary>
/// <param name="route">The route to remove.</param>
void Remove(string route);
/// <summary>
/// Adds a range of routes to the table.
/// </summary>
/// <param name="routes">The routes to add.</param>
void AddRange(IEnumerable<string> routes);
/// <summary>
/// Removes a range of routes from the table.
/// </summary>
/// <param name="routes">The routes to remove.</param>
void RemoveRange(IEnumerable<string> routes);
}

View file

@ -0,0 +1,14 @@
// ReSharper disable once CheckNamespace
namespace Elsa.Extensions;
/// <summary>
/// Provides extension methods for strings that represent a route.
/// </summary>
public static class RouteExtensions
{
/// <summary>
/// Normalizes a route by ensuring a leading slash, removing any trailing slash and converting the path to lowercase.
/// </summary>
public static string NormalizeRoute(this string path) => $"/{path.Trim('/').ToLowerInvariant()}";
}

View file

@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Elsa.Http.Contracts;
using Elsa.Http.Models;
using Elsa.Http.Options;
@ -10,6 +13,8 @@ using Microsoft.Extensions.Options;
using System.Net;
using System.Net.Mime;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Extensions;
using Elsa.Http.Bookmarks;
using Elsa.Workflows.Core;
@ -85,7 +90,7 @@ public class WorkflowsMiddleware
public async Task InvokeAsync(HttpContext httpContext)
{
var path = GetPath(httpContext);
var basePath = _options.BasePath;
var basePath = _options.BasePath?.ToString().NormalizeRoute();
// If the request path does not match the configured base path to handle workflows, then skip.
if (!string.IsNullOrWhiteSpace(basePath))
@ -97,7 +102,7 @@ public class WorkflowsMiddleware
}
// Strip the base path.
path = path[basePath.Value.Value!.Length..];
path = path[basePath.Length..];
}
var matchingPath = GetMatchingRoute(path);
@ -249,7 +254,7 @@ public class WorkflowsMiddleware
}
}
private string GetPath(HttpContext httpContext) => httpContext.Request.Path.Value!.ToLowerInvariant();
private string GetPath(HttpContext httpContext) => httpContext.Request.Path.Value!.NormalizeRoute();
private async Task<bool> HandleNoWorkflowsFoundAsync(HttpContext httpContext, ICollection<WorkflowMatch> workflowMatches, PathString? basePath)
{

View file

@ -1,5 +1,6 @@
// Taken & adapted from https://github.com/markvincze/Stubbery/blob/main/src/Stubbery/RequestMatching/RouteMatcher.cs
using Elsa.Extensions;
using Elsa.Http.Contracts;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Template;
@ -10,13 +11,14 @@ namespace Elsa.Http.Services;
public class RouteMatcher : IRouteMatcher
{
/// <inheritdoc />
public RouteValueDictionary? Match(string routeTemplate, string requestPath)
public RouteValueDictionary? Match(string routeTemplate, string route)
{
var normalizedRoute = route.NormalizeRoute();
var template = TemplateParser.Parse(routeTemplate);
var matcher = new TemplateMatcher(template, GetDefaults(template));
var values = new RouteValueDictionary();
return matcher.TryMatch(requestPath, values) ? values : null;
return matcher.TryMatch(normalizedRoute, values) ? values : null;
}
private static RouteValueDictionary GetDefaults(RouteTemplate parsedTemplate)

View file

@ -1,5 +1,7 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using Elsa.Extensions;
using Elsa.Http.Contracts;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
@ -25,30 +27,35 @@ public class RouteTable : IRouteTable
private ConcurrentDictionary<string, string> Routes => _cache.GetOrCreate(Key, _ => new ConcurrentDictionary<string, string>())!;
/// <inheritdoc />
public void Add(string path)
public void Add(string route)
{
if (path.Contains("//"))
if (route.Contains("//"))
{
_logger.LogWarning("Path cannot contain double slashes. Ignoring path: {Path}", path);
_logger.LogWarning("Path cannot contain double slashes. Ignoring path: {Path}", route);
return;
}
Routes.TryAdd(path, path);
var normalizedRoute = route.NormalizeRoute();
Routes.TryAdd(normalizedRoute, normalizedRoute);
}
/// <inheritdoc />
public void Remove(string path) => Routes.TryRemove(path, out _);
/// <inheritdoc />
public void AddRange(IEnumerable<string> paths)
public void Remove(string route)
{
foreach (var path in paths) Add(path);
var normalizedRoute = route.NormalizeRoute();
Routes.TryRemove(normalizedRoute, out _);
}
/// <inheritdoc />
public void RemoveRange(IEnumerable<string> paths)
public void AddRange(IEnumerable<string> routes)
{
foreach (var path in paths) Remove(path);
foreach (var route in routes) Add(route);
}
/// <inheritdoc />
public void RemoveRange(IEnumerable<string> routes)
{
foreach (var route in routes) Remove(route);
}
/// <inheritdoc />