Skip to content

Commit f594112

Browse files
Adds support for server path variables. Closes #1292 (#1296)
1 parent 0ce8a04 commit f594112

File tree

3 files changed

+56
-17
lines changed

3 files changed

+56
-17
lines changed

DevProxy.Abstractions/Utils/ProxyUtils.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,15 @@ public static IEnumerable<string> GetWildcardPatterns(ReadOnlyCollection<string>
509509
.OrderBy(x => x)];
510510
}
511511

512+
#pragma warning disable CA1055
513+
public static string UrlWithParametersToRegex(string urlWithParameters)
514+
#pragma warning restore CA1055
515+
{
516+
ArgumentNullException.ThrowIfNull(urlWithParameters);
517+
518+
return $"^{Regex.Replace(Regex.Escape(urlWithParameters), "\\\\{[^}]+}", ".*")}";
519+
}
520+
512521
internal static Assembly GetAssembly()
513522
=> _assembly ??= (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly());
514523

DevProxy.Plugins/Extensions/OpenApiDocumentExtensions.cs

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See the LICENSE file in the project root for more information.
44

55
using DevProxy.Abstractions.Proxy;
6+
using DevProxy.Abstractions.Utils;
67
using DevProxy.Plugins.Models;
78
using DevProxy.Plugins.Utils;
89
using Microsoft.Extensions.Logging;
@@ -116,27 +117,26 @@ [.. operationsFromRequests
116117
{
117118
logger.LogDebug("Checking server URL {ServerUrl}...", server.Url);
118119

119-
if (!requestUrl.StartsWith(server.Url, StringComparison.OrdinalIgnoreCase))
120+
if (!UrlMatchesServerUrl(requestUrl, server.Url))
120121
{
121122
logger.LogDebug("Request URL {RequestUrl} does not match server URL {ServerUrl}", requestUrl, server.Url);
122123
continue;
123124
}
124125

125-
var serverUrl = new Uri(server.Url);
126-
var serverPath = serverUrl.AbsolutePath.TrimEnd('/');
127126
var requestUri = new Uri(requestUrl);
128-
var urlPathFromRequest = requestUri.GetLeftPart(UriPartial.Path).Replace(server.Url.TrimEnd('/'), "", StringComparison.OrdinalIgnoreCase);
127+
var absoluteUrlPathFromRequest = requestUri.GetLeftPart(UriPartial.Path);
129128

130129
foreach (var path in openApiDocument.Paths)
131130
{
132131
var urlPathFromSpec = path.Key;
133-
logger.LogDebug("Checking path {UrlPath}...", urlPathFromSpec);
132+
var absolutePathFromSpec = server.Url.TrimEnd('/') + urlPathFromSpec;
133+
logger.LogDebug("Checking path {UrlPath}...", absolutePathFromSpec);
134134

135135
// check if path contains parameters. If it does,
136136
// replace them with regex
137-
if (urlPathFromSpec.Contains('{', StringComparison.OrdinalIgnoreCase))
137+
if (absolutePathFromSpec.Contains('{', StringComparison.OrdinalIgnoreCase))
138138
{
139-
logger.LogDebug("Path {UrlPath} contains parameters and will be converted to Regex", urlPathFromSpec);
139+
logger.LogDebug("Path {UrlPath} contains parameters and will be converted to Regex", absolutePathFromSpec);
140140

141141
// force replace all parameters with regex
142142
// this is more robust than replacing parameters by name
@@ -147,24 +147,24 @@ [.. operationsFromRequests
147147
// we also escape the path to make sure that regex special
148148
// characters are not interpreted so that we won't fail
149149
// on matching URLs that contain ()
150-
urlPathFromSpec = Regex.Replace(Regex.Escape(urlPathFromSpec), @"\\\{[^}]+\}", $"([^/]+)");
150+
absolutePathFromSpec = Regex.Replace(Regex.Escape(absolutePathFromSpec), @"\\\{[^}]+\}", $"([^/]+)");
151151

152-
logger.LogDebug("Converted path to Regex: {UrlPath}", urlPathFromSpec);
153-
var regex = new Regex($"^{urlPathFromSpec}$");
154-
if (regex.IsMatch(urlPathFromRequest))
152+
logger.LogDebug("Converted path to Regex: {UrlPath}", absolutePathFromSpec);
153+
var regex = new Regex($"^{absolutePathFromSpec}$");
154+
if (regex.IsMatch(absoluteUrlPathFromRequest))
155155
{
156-
logger.LogDebug("Regex matches {RequestUrl}", urlPathFromRequest);
156+
logger.LogDebug("Regex matches {RequestUrl}", absoluteUrlPathFromRequest);
157157

158158
return path;
159159
}
160160

161-
logger.LogDebug("Regex does not match {RequestUrl}", urlPathFromRequest);
161+
logger.LogDebug("Regex does not match {RequestUrl}", absoluteUrlPathFromRequest);
162162
}
163163
else
164164
{
165-
if (urlPathFromRequest.Equals(urlPathFromSpec, StringComparison.OrdinalIgnoreCase))
165+
if (absoluteUrlPathFromRequest.Equals(absolutePathFromSpec, StringComparison.OrdinalIgnoreCase))
166166
{
167-
logger.LogDebug("{RequestUrl} matches {UrlPath}", requestUrl, urlPathFromSpec);
167+
logger.LogDebug("{RequestUrl} matches {UrlPath}", requestUrl, absolutePathFromSpec);
168168
return path;
169169
}
170170

@@ -216,4 +216,21 @@ public static OpenApiSecurityScheme[] GetOAuth2Schemes(this OpenApiDocument open
216216
.Where(s => s.Value.Type == SecuritySchemeType.OAuth2)
217217
.Select(s => s.Value)];
218218
}
219+
220+
private static bool UrlMatchesServerUrl(string absoluteUrl, string serverUrl)
221+
{
222+
if (absoluteUrl.StartsWith(serverUrl, StringComparison.OrdinalIgnoreCase))
223+
{
224+
return true;
225+
}
226+
227+
// If serverUrl contains parameters, use regex to compare it
228+
if (!serverUrl.Contains('{', StringComparison.Ordinal))
229+
{
230+
return false;
231+
}
232+
233+
var serverUrlPattern = ProxyUtils.UrlWithParametersToRegex(serverUrl);
234+
return Regex.IsMatch(absoluteUrl, serverUrlPattern, RegexOptions.IgnoreCase);
235+
}
219236
}

DevProxy.Plugins/Reporting/MinimalPermissionsPlugin.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Microsoft.Extensions.Logging;
1111
using Microsoft.OpenApi.Models;
1212
using Microsoft.OpenApi.Readers;
13+
using System.Text.RegularExpressions;
1314

1415
namespace DevProxy.Plugins.Reporting;
1516

@@ -185,7 +186,17 @@ private async Task<Dictionary<string, OpenApiDocument>> LoadApiSpecsAsync(string
185186
Logger.LogDebug("No URL found for server '{Server}'", server.Description ?? "unnamed");
186187
continue;
187188
}
188-
apiDefinitions[server.Url] = apiDefinition;
189+
190+
Logger.LogDebug("Found server '{Server}' with URL '{Url}'", server.Description ?? "unnamed", server.Url);
191+
192+
var serverUrl = server.Url;
193+
if (server.Url.Contains('{', StringComparison.Ordinal))
194+
{
195+
serverUrl = ProxyUtils.UrlWithParametersToRegex(server.Url);
196+
Logger.LogDebug("Transformed server URL '{OriginalUrl}' to '{TransformedUrl}'", server.Url, serverUrl);
197+
}
198+
199+
apiDefinitions[serverUrl] = apiDefinition;
189200
}
190201
}
191202
catch (Exception ex)
@@ -205,7 +216,9 @@ private async Task<Dictionary<string, OpenApiDocument>> LoadApiSpecsAsync(string
205216
var url = request.Message.Split(' ')[1];
206217
Logger.LogDebug("Matching request {RequestUrl} to API specs...", url);
207218

208-
var matchingKey = apiSpecsByUrl.Keys.FirstOrDefault(url.StartsWith);
219+
var matchingKey = apiSpecsByUrl.Keys.FirstOrDefault(urlOrPattern =>
220+
url.StartsWith(urlOrPattern, StringComparison.OrdinalIgnoreCase) ||
221+
Regex.IsMatch(url, urlOrPattern));
209222
if (matchingKey is null)
210223
{
211224
Logger.LogDebug("No matching API spec found for {RequestUrl}", url);

0 commit comments

Comments
 (0)