diff --git a/DevProxy.Abstractions/Utils/ProxyUtils.cs b/DevProxy.Abstractions/Utils/ProxyUtils.cs index dacb264c..ec21a389 100644 --- a/DevProxy.Abstractions/Utils/ProxyUtils.cs +++ b/DevProxy.Abstractions/Utils/ProxyUtils.cs @@ -509,6 +509,15 @@ public static IEnumerable GetWildcardPatterns(ReadOnlyCollection .OrderBy(x => x)]; } +#pragma warning disable CA1055 + public static string UrlWithParametersToRegex(string urlWithParameters) +#pragma warning restore CA1055 + { + ArgumentNullException.ThrowIfNull(urlWithParameters); + + return $"^{Regex.Replace(Regex.Escape(urlWithParameters), "\\\\{[^}]+}", ".*")}"; + } + internal static Assembly GetAssembly() => _assembly ??= (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()); diff --git a/DevProxy.Plugins/Extensions/OpenApiDocumentExtensions.cs b/DevProxy.Plugins/Extensions/OpenApiDocumentExtensions.cs index fe008ccd..9458db4d 100644 --- a/DevProxy.Plugins/Extensions/OpenApiDocumentExtensions.cs +++ b/DevProxy.Plugins/Extensions/OpenApiDocumentExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using DevProxy.Abstractions.Proxy; +using DevProxy.Abstractions.Utils; using DevProxy.Plugins.Models; using DevProxy.Plugins.Utils; using Microsoft.Extensions.Logging; @@ -116,27 +117,26 @@ [.. operationsFromRequests { logger.LogDebug("Checking server URL {ServerUrl}...", server.Url); - if (!requestUrl.StartsWith(server.Url, StringComparison.OrdinalIgnoreCase)) + if (!UrlMatchesServerUrl(requestUrl, server.Url)) { logger.LogDebug("Request URL {RequestUrl} does not match server URL {ServerUrl}", requestUrl, server.Url); continue; } - var serverUrl = new Uri(server.Url); - var serverPath = serverUrl.AbsolutePath.TrimEnd('/'); var requestUri = new Uri(requestUrl); - var urlPathFromRequest = requestUri.GetLeftPart(UriPartial.Path).Replace(server.Url.TrimEnd('/'), "", StringComparison.OrdinalIgnoreCase); + var absoluteUrlPathFromRequest = requestUri.GetLeftPart(UriPartial.Path); foreach (var path in openApiDocument.Paths) { var urlPathFromSpec = path.Key; - logger.LogDebug("Checking path {UrlPath}...", urlPathFromSpec); + var absolutePathFromSpec = server.Url.TrimEnd('/') + urlPathFromSpec; + logger.LogDebug("Checking path {UrlPath}...", absolutePathFromSpec); // check if path contains parameters. If it does, // replace them with regex - if (urlPathFromSpec.Contains('{', StringComparison.OrdinalIgnoreCase)) + if (absolutePathFromSpec.Contains('{', StringComparison.OrdinalIgnoreCase)) { - logger.LogDebug("Path {UrlPath} contains parameters and will be converted to Regex", urlPathFromSpec); + logger.LogDebug("Path {UrlPath} contains parameters and will be converted to Regex", absolutePathFromSpec); // force replace all parameters with regex // this is more robust than replacing parameters by name @@ -147,24 +147,24 @@ [.. operationsFromRequests // we also escape the path to make sure that regex special // characters are not interpreted so that we won't fail // on matching URLs that contain () - urlPathFromSpec = Regex.Replace(Regex.Escape(urlPathFromSpec), @"\\\{[^}]+\}", $"([^/]+)"); + absolutePathFromSpec = Regex.Replace(Regex.Escape(absolutePathFromSpec), @"\\\{[^}]+\}", $"([^/]+)"); - logger.LogDebug("Converted path to Regex: {UrlPath}", urlPathFromSpec); - var regex = new Regex($"^{urlPathFromSpec}$"); - if (regex.IsMatch(urlPathFromRequest)) + logger.LogDebug("Converted path to Regex: {UrlPath}", absolutePathFromSpec); + var regex = new Regex($"^{absolutePathFromSpec}$"); + if (regex.IsMatch(absoluteUrlPathFromRequest)) { - logger.LogDebug("Regex matches {RequestUrl}", urlPathFromRequest); + logger.LogDebug("Regex matches {RequestUrl}", absoluteUrlPathFromRequest); return path; } - logger.LogDebug("Regex does not match {RequestUrl}", urlPathFromRequest); + logger.LogDebug("Regex does not match {RequestUrl}", absoluteUrlPathFromRequest); } else { - if (urlPathFromRequest.Equals(urlPathFromSpec, StringComparison.OrdinalIgnoreCase)) + if (absoluteUrlPathFromRequest.Equals(absolutePathFromSpec, StringComparison.OrdinalIgnoreCase)) { - logger.LogDebug("{RequestUrl} matches {UrlPath}", requestUrl, urlPathFromSpec); + logger.LogDebug("{RequestUrl} matches {UrlPath}", requestUrl, absolutePathFromSpec); return path; } @@ -216,4 +216,21 @@ public static OpenApiSecurityScheme[] GetOAuth2Schemes(this OpenApiDocument open .Where(s => s.Value.Type == SecuritySchemeType.OAuth2) .Select(s => s.Value)]; } + + private static bool UrlMatchesServerUrl(string absoluteUrl, string serverUrl) + { + if (absoluteUrl.StartsWith(serverUrl, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // If serverUrl contains parameters, use regex to compare it + if (!serverUrl.Contains('{', StringComparison.Ordinal)) + { + return false; + } + + var serverUrlPattern = ProxyUtils.UrlWithParametersToRegex(serverUrl); + return Regex.IsMatch(absoluteUrl, serverUrlPattern, RegexOptions.IgnoreCase); + } } \ No newline at end of file diff --git a/DevProxy.Plugins/Reporting/MinimalPermissionsPlugin.cs b/DevProxy.Plugins/Reporting/MinimalPermissionsPlugin.cs index 5f0b6fff..d4134458 100644 --- a/DevProxy.Plugins/Reporting/MinimalPermissionsPlugin.cs +++ b/DevProxy.Plugins/Reporting/MinimalPermissionsPlugin.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Readers; +using System.Text.RegularExpressions; namespace DevProxy.Plugins.Reporting; @@ -185,7 +186,17 @@ private async Task> LoadApiSpecsAsync(string Logger.LogDebug("No URL found for server '{Server}'", server.Description ?? "unnamed"); continue; } - apiDefinitions[server.Url] = apiDefinition; + + Logger.LogDebug("Found server '{Server}' with URL '{Url}'", server.Description ?? "unnamed", server.Url); + + var serverUrl = server.Url; + if (server.Url.Contains('{', StringComparison.Ordinal)) + { + serverUrl = ProxyUtils.UrlWithParametersToRegex(server.Url); + Logger.LogDebug("Transformed server URL '{OriginalUrl}' to '{TransformedUrl}'", server.Url, serverUrl); + } + + apiDefinitions[serverUrl] = apiDefinition; } } catch (Exception ex) @@ -205,7 +216,9 @@ private async Task> LoadApiSpecsAsync(string var url = request.Message.Split(' ')[1]; Logger.LogDebug("Matching request {RequestUrl} to API specs...", url); - var matchingKey = apiSpecsByUrl.Keys.FirstOrDefault(url.StartsWith); + var matchingKey = apiSpecsByUrl.Keys.FirstOrDefault(urlOrPattern => + url.StartsWith(urlOrPattern, StringComparison.OrdinalIgnoreCase) || + Regex.IsMatch(url, urlOrPattern)); if (matchingKey is null) { Logger.LogDebug("No matching API spec found for {RequestUrl}", url);