Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 67 additions & 13 deletions DevProxy.Plugins/Mocking/CrudApiPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System.Security.Claims;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Web;
using Titanium.Web.Proxy.EventArguments;
using Titanium.Web.Proxy.Http;
using Titanium.Web.Proxy.Models;
Expand Down Expand Up @@ -246,6 +247,7 @@ private async Task SetupOpenIdConnectConfigurationAsync()
});

var requestUrlWithoutQuery = request.RequestUri.GetLeftPart(UriPartial.Path);
var requestQuery = request.RequestUri.Query;
var parameters = new Dictionary<string, string>();
var action = Configuration.Actions.FirstOrDefault(action =>
{
Expand All @@ -256,33 +258,85 @@ private async Task SetupOpenIdConnectConfigurationAsync()

var absoluteActionUrl = (Configuration.BaseUrl + action.Url).Replace("//", "/", 8);

if (absoluteActionUrl == requestUrlWithoutQuery)
// split action URL into path and query parts
string actionPath;
string? actionQuery = null;
var queryIndex = absoluteActionUrl.IndexOf('?', StringComparison.OrdinalIgnoreCase);
if (queryIndex >= 0)
{
return true;
actionPath = absoluteActionUrl[..queryIndex];
actionQuery = absoluteActionUrl[queryIndex..];
}
else
{
actionPath = absoluteActionUrl;
}

// check if the action contains parameters
// if it doesn't, it's not a match for the current request for sure
if (!absoluteActionUrl.Contains('{', StringComparison.OrdinalIgnoreCase))
// match the path part
var pathMatched = false;
if (actionPath == requestUrlWithoutQuery)
{
return false;
pathMatched = true;
}
else if (actionPath.Contains('{', StringComparison.OrdinalIgnoreCase))
{
var pathRegex = Regex.Replace(Regex.Escape(actionPath).Replace("\\{", "{", StringComparison.OrdinalIgnoreCase), "({[^}]+})", parameterMatchEvaluator);
var pathMatch = Regex.Match(requestUrlWithoutQuery, pathRegex);
if (pathMatch.Success)
{
pathMatched = true;
foreach (var groupName in pathMatch.Groups.Keys)
{
if (groupName == "0")
{
continue;
}
parameters[groupName] = Uri.UnescapeDataString(pathMatch.Groups[groupName].Value);
}
}
}

// convert parameters into named regex groups
var urlRegex = Regex.Replace(Regex.Escape(absoluteActionUrl).Replace("\\{", "{", StringComparison.OrdinalIgnoreCase), "({[^}]+})", parameterMatchEvaluator);
var match = Regex.Match(requestUrlWithoutQuery, urlRegex);
if (!match.Success)
if (!pathMatched)
{
return false;
}

foreach (var groupName in match.Groups.Keys)
// if the action has no query string definition, it's a match
if (string.IsNullOrEmpty(actionQuery))
{
return true;
}

// match query string parameters individually (order-independent)
var actionQueryParams = HttpUtility.ParseQueryString(actionQuery.TrimStart('?'));
var requestQueryParams = HttpUtility.ParseQueryString(requestQuery.TrimStart('?'));

foreach (string? key in actionQueryParams.AllKeys)
{
if (groupName == "0")
if (key is null)
{
continue;
}
parameters.Add(groupName, Uri.UnescapeDataString(match.Groups[groupName].Value));

var actionValue = actionQueryParams[key] ?? string.Empty;
var requestValue = requestQueryParams[key];

if (requestValue is null)
{
return false;
}

// check if the action value is a parameter pattern like {param}
var paramMatch = Regex.Match(actionValue, "^{([^}]+)}$");
if (paramMatch.Success)
{
var paramName = paramMatch.Groups[1].Value.Replace('-', '_');
parameters[paramName] = Uri.UnescapeDataString(requestValue);
}
else if (!string.Equals(actionValue, requestValue, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
return true;
});
Expand Down
Loading