using System; using System.Collections; using System.Collections.Generic; using System.Linq; using ImpromptuInterface; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.DependencyInjection; namespace Lidarr.Http.Validation { public interface IDfaMatcherBuilder { void AddEndpoint(RouteEndpoint endpoint); object BuildDfaTree(bool includeLabel = false); } // https://github.com/dotnet/aspnetcore/blob/cc3d47f5501cdfae3e5b5be509ef2c0fb8cca069/src/Http/Routing/src/Matching/DfaNode.cs public interface IDfaNode { string Label { get; set; } List Matches { get; } IDictionary Literals { get; } object Parameters { get; } object CatchAll { get; } IDictionary PolicyEdges { get; } } public class DuplicateEndpointDetector { private readonly IServiceProvider _services; public DuplicateEndpointDetector(IServiceProvider services) { _services = services; } public Dictionary> GetDuplicateEndpoints(EndpointDataSource dataSource) { // get the DfaMatcherBuilder - internal, so needs reflection :( var matcherBuilder = typeof(IEndpointSelectorPolicy).Assembly .GetType("Microsoft.AspNetCore.Routing.Matching.DfaMatcherBuilder"); var rawBuilder = _services.GetRequiredService(matcherBuilder); var builder = rawBuilder.ActLike(); var endpoints = dataSource.Endpoints; foreach (var t in endpoints) { if (t is RouteEndpoint endpoint && (endpoint.Metadata.GetMetadata()?.SuppressMatching ?? false) == false) { builder.AddEndpoint(endpoint); } } // Assign each node a sequential index. var visited = new Dictionary(); var duplicates = new Dictionary>(); var rawTree = builder.BuildDfaTree(includeLabel: true); Visit(rawTree, LogDuplicates); return duplicates; void LogDuplicates(IDfaNode node) { if (!visited.TryGetValue(node, out var label)) { label = visited.Count; visited.Add(node, label); } // We can safely index into visited because this is a post-order traversal, // all of the children of this node are already in the dictionary. var filteredMatches = node?.Matches?.Where(x => !x.DisplayName.StartsWith("Lidarr.Http.Frontend.StaticResourceController")).ToList(); var matchCount = filteredMatches?.Count ?? 0; if (matchCount > 1) { var duplicateEndpoints = filteredMatches.Select(x => x.DisplayName).ToList(); duplicates[node.Label] = duplicateEndpoints; } } } private static void Visit(object rawNode, Action visitor) { var node = rawNode.ActLike(); if (node.Literals?.Values != null) { foreach (var dictValue in node.Literals.Values) { Visit(dictValue, visitor); } } // Break cycles if (node.Parameters != null && !ReferenceEquals(rawNode, node.Parameters)) { Visit(node.Parameters, visitor); } // Break cycles if (node.CatchAll != null && !ReferenceEquals(rawNode, node.CatchAll)) { Visit(node.CatchAll, visitor); } if (node.PolicyEdges?.Values != null) { foreach (var dictValue in node.PolicyEdges.Values) { Visit(dictValue, visitor); } } visitor(node); } } }