Added support for query string parameters in upstream path template (#467)

This commit is contained in:
Tom Pallister
2018-07-12 19:26:23 +01:00
committed by GitHub
parent 19ea93d10e
commit 8f4ae03290
24 changed files with 664 additions and 143 deletions

View File

@ -14,7 +14,8 @@ namespace Ocelot.Configuration.Creator
public UpstreamPathTemplate Create(IReRoute reRoute)
{
var upstreamTemplate = reRoute.UpstreamPathTemplate;
var upstreamTemplate = reRoute.UpstreamPathTemplate;
var placeholders = new List<string>();
@ -30,9 +31,17 @@ namespace Ocelot.Configuration.Creator
//hack to handle /{url} case
if(ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket))
{
return new UpstreamPathTemplate(RegExForwardSlashAndOnePlaceHolder, 0);
return new UpstreamPathTemplate(RegExForwardSlashAndOnePlaceHolder, 0, false);
}
}
}
var containsQueryString = false;
if (upstreamTemplate.Contains("?"))
{
containsQueryString = true;
upstreamTemplate = upstreamTemplate.Replace("?", "\\?");
}
foreach (var placeholder in placeholders)
@ -42,7 +51,7 @@ namespace Ocelot.Configuration.Creator
if (upstreamTemplate == "/")
{
return new UpstreamPathTemplate(RegExForwardSlashOnly, reRoute.Priority);
return new UpstreamPathTemplate(RegExForwardSlashOnly, reRoute.Priority, containsQueryString);
}
if(upstreamTemplate.EndsWith("/"))
@ -54,7 +63,7 @@ namespace Ocelot.Configuration.Creator
? $"^{upstreamTemplate}{RegExMatchEndString}"
: $"^{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}";
return new UpstreamPathTemplate(route, reRoute.Priority);
return new UpstreamPathTemplate(route, reRoute.Priority, containsQueryString);
}
private bool ForwardSlashAndOnePlaceHolder(string upstreamTemplate, List<string> placeholders, int postitionOfPlaceHolderClosingBracket)

View File

@ -21,7 +21,7 @@
_cache = new ConcurrentDictionary<string, OkResponse<DownstreamRoute>>();
}
public Response<DownstreamRoute> Get(string upstreamUrlPath, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost)
public Response<DownstreamRoute> Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost)
{
var serviceName = GetServiceName(upstreamUrlPath);

View File

@ -18,7 +18,7 @@ namespace Ocelot.DownstreamRouteFinder.Finder
_placeholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder;
}
public Response<DownstreamRoute> Get(string path, string httpMethod, IInternalConfiguration configuration, string upstreamHost)
public Response<DownstreamRoute> Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod, IInternalConfiguration configuration, string upstreamHost)
{
var downstreamRoutes = new List<DownstreamRoute>();
@ -28,11 +28,11 @@ namespace Ocelot.DownstreamRouteFinder.Finder
foreach (var reRoute in applicableReRoutes)
{
var urlMatch = _urlMatcher.Match(path, reRoute.UpstreamTemplatePattern.Template);
var urlMatch = _urlMatcher.Match(upstreamUrlPath, upstreamQueryString, reRoute.UpstreamTemplatePattern.Template, reRoute.UpstreamTemplatePattern.ContainsQueryString);
if (urlMatch.Data.Match)
{
downstreamRoutes.Add(GetPlaceholderNamesAndValues(path, reRoute));
downstreamRoutes.Add(GetPlaceholderNamesAndValues(upstreamUrlPath, upstreamQueryString, reRoute));
}
}
@ -44,7 +44,7 @@ namespace Ocelot.DownstreamRouteFinder.Finder
return notNullOption != null ? new OkResponse<DownstreamRoute>(notNullOption) : new OkResponse<DownstreamRoute>(nullOption);
}
return new ErrorResponse<DownstreamRoute>(new UnableToFindDownstreamRouteError(path, httpMethod));
return new ErrorResponse<DownstreamRoute>(new UnableToFindDownstreamRouteError(upstreamUrlPath, httpMethod));
}
private bool RouteIsApplicableToThisRequest(ReRoute reRoute, string httpMethod, string upstreamHost)
@ -53,9 +53,9 @@ namespace Ocelot.DownstreamRouteFinder.Finder
(string.IsNullOrEmpty(reRoute.UpstreamHost) || reRoute.UpstreamHost == upstreamHost);
}
private DownstreamRoute GetPlaceholderNamesAndValues(string path, ReRoute reRoute)
private DownstreamRoute GetPlaceholderNamesAndValues(string path, string query, ReRoute reRoute)
{
var templatePlaceholderNameAndValues = _placeholderNameAndValueFinder.Find(path, reRoute.UpstreamPathTemplate.Value);
var templatePlaceholderNameAndValues = _placeholderNameAndValueFinder.Find(path, query, reRoute.UpstreamPathTemplate.Value);
return new DownstreamRoute(templatePlaceholderNameAndValues.Data, reRoute);
}

View File

@ -6,6 +6,6 @@ namespace Ocelot.DownstreamRouteFinder.Finder
{
public interface IDownstreamRouteProvider
{
Response<DownstreamRoute> Get(string upstreamUrlPath, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost);
Response<DownstreamRoute> Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost);
}
}

View File

@ -33,13 +33,15 @@ namespace Ocelot.DownstreamRouteFinder.Middleware
{
var upstreamUrlPath = context.HttpContext.Request.Path.ToString();
var upstreamQueryString = context.HttpContext.Request.QueryString.ToString();
var upstreamHost = context.HttpContext.Request.Headers["Host"];
Logger.LogDebug($"Upstream url path is {upstreamUrlPath}");
var provider = _factory.Get(context.Configuration);
var downstreamRoute = provider.Get(upstreamUrlPath, context.HttpContext.Request.Method, context.Configuration, upstreamHost);
var downstreamRoute = provider.Get(upstreamUrlPath, upstreamQueryString, context.HttpContext.Request.Method, context.Configuration, upstreamHost);
if (downstreamRoute.IsError)
{

View File

@ -5,6 +5,6 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher
{
public interface IPlaceholderNameAndValueFinder
{
Response<List<PlaceholderNameAndValue>> Find(string path, string pathTemplate);
Response<List<PlaceholderNameAndValue>> Find(string path, string query, string pathTemplate);
}
}

View File

@ -4,6 +4,6 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher
{
public interface IUrlPathToUrlTemplateMatcher
{
Response<UrlMatch> Match(string upstreamUrlPath, string upstreamUrlPathTemplate);
Response<UrlMatch> Match(string upstreamUrlPath, string upstreamQueryString, string upstreamUrlPathTemplate, bool containsQueryString);
}
}
}

View File

@ -5,11 +5,18 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher
{
public class RegExUrlMatcher : IUrlPathToUrlTemplateMatcher
{
public Response<UrlMatch> Match(string upstreamUrlPath, string upstreamUrlPathTemplate)
public Response<UrlMatch> Match(string upstreamUrlPath, string upstreamQueryString, string upstreamUrlPathTemplate, bool containsQueryString)
{
var regex = new Regex(upstreamUrlPathTemplate);
return regex.IsMatch(upstreamUrlPath)
if (!containsQueryString)
{
return regex.IsMatch(upstreamUrlPath)
? new OkResponse<UrlMatch>(new UrlMatch(true))
: new OkResponse<UrlMatch>(new UrlMatch(false));
}
return regex.IsMatch($"{upstreamUrlPath}{upstreamQueryString}")
? new OkResponse<UrlMatch>(new UrlMatch(true))
: new OkResponse<UrlMatch>(new UrlMatch(false));
}

View File

@ -5,27 +5,46 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher
{
public class UrlPathPlaceholderNameAndValueFinder : IPlaceholderNameAndValueFinder
{
public Response<List<PlaceholderNameAndValue>> Find(string path, string pathTemplate)
public Response<List<PlaceholderNameAndValue>> Find(string path, string query, string pathTemplate)
{
var placeHolderNameAndValues = new List<PlaceholderNameAndValue>();
path = $"{path}{query}";
int counterForPath = 0;
var delimiter = '/';
var nextDelimiter = '/';
for (int counterForTemplate = 0; counterForTemplate < pathTemplate.Length; counterForTemplate++)
{
if ((path.Length > counterForPath) && CharactersDontMatch(pathTemplate[counterForTemplate], path[counterForPath]) && ContinueScanningUrl(counterForPath,path.Length))
{
if (IsPlaceholder(pathTemplate[counterForTemplate]))
{
//should_find_multiple_query_string make test pass
if (PassedQueryString(pathTemplate, counterForTemplate))
{
delimiter = '&';
nextDelimiter = '&';
}
//should_find_multiple_query_string_and_path makes test pass
if (NotPassedQueryString(pathTemplate, counterForTemplate) && NoMoreForwardSlash(pathTemplate, counterForTemplate))
{
delimiter = '?';
nextDelimiter = '?';
}
var placeholderName = GetPlaceholderName(pathTemplate, counterForTemplate);
var placeholderValue = GetPlaceholderValue(pathTemplate, placeholderName, path, counterForPath);
var placeholderValue = GetPlaceholderValue(pathTemplate, query, placeholderName, path, counterForPath, delimiter);
placeHolderNameAndValues.Add(new PlaceholderNameAndValue(placeholderName, placeholderValue));
counterForTemplate = GetNextCounterPosition(pathTemplate, counterForTemplate, '}');
counterForPath = GetNextCounterPosition(path, counterForPath, '/');
counterForPath = GetNextCounterPosition(path, counterForPath, nextDelimiter);
continue;
}
@ -44,7 +63,7 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher
}
else
{
var placeholderValue = GetPlaceholderValue(pathTemplate, placeholderName, path, counterForPath + 1);
var placeholderValue = GetPlaceholderValue(pathTemplate, query, placeholderName, path, counterForPath + 1, '/');
placeHolderNameAndValues.Add(new PlaceholderNameAndValue(placeholderName, placeholderValue));
}
@ -57,6 +76,21 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher
return new OkResponse<List<PlaceholderNameAndValue>>(placeHolderNameAndValues);
}
private static bool NoMoreForwardSlash(string pathTemplate, int counterForTemplate)
{
return !pathTemplate.Substring(counterForTemplate).Contains("/");
}
private static bool NotPassedQueryString(string pathTemplate, int counterForTemplate)
{
return !pathTemplate.Substring(0, counterForTemplate).Contains("?");
}
private static bool PassedQueryString(string pathTemplate, int counterForTemplate)
{
return pathTemplate.Substring(0, counterForTemplate).Contains("?");
}
private bool IsCatchAll(string path, int counterForPath, string pathTemplate)
{
return string.IsNullOrEmpty(path) || (path.Length > counterForPath && path[counterForPath] == '/') && pathTemplate.Length > 1
@ -69,11 +103,11 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher
return path.Length == 1 || path.Length == 0;
}
private string GetPlaceholderValue(string urlPathTemplate, string variableName, string urlPath, int counterForUrl)
private string GetPlaceholderValue(string urlPathTemplate, string query, string variableName, string urlPath, int counterForUrl, char delimiter)
{
var positionOfNextSlash = urlPath.IndexOf('/', counterForUrl);
var positionOfNextSlash = urlPath.IndexOf(delimiter, counterForUrl);
if (positionOfNextSlash == -1 || urlPathTemplate.Trim('/').EndsWith(variableName))
if (positionOfNextSlash == -1 || (urlPathTemplate.Trim(delimiter).EndsWith(variableName) && string.IsNullOrEmpty(query)))
{
positionOfNextSlash = urlPath.Length;
}

View File

@ -1,17 +1,15 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging;
using Ocelot.Middleware;
using System;
using System.Linq;
using Ocelot.DownstreamRouteFinder.Middleware;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator.Middleware
{
using System.Text.RegularExpressions;
public class DownstreamUrlCreatorMiddleware : OcelotMiddleware
{
private readonly OcelotRequestDelegate _next;
@ -55,12 +53,11 @@ namespace Ocelot.DownstreamUrlCreator.Middleware
{
context.DownstreamRequest.AbsolutePath = GetPath(dsPath);
context.DownstreamRequest.Query = GetQueryString(dsPath);
// todo - do we need to add anything from the request query string onto the query from the
// templae?
}
else
else
{
RemoveQueryStringParametersThatHaveBeenUsedInTemplate(context);
context.DownstreamRequest.AbsolutePath = dsPath.Value;
}
}
@ -70,14 +67,37 @@ namespace Ocelot.DownstreamUrlCreator.Middleware
await _next.Invoke(context);
}
private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(DownstreamContext context)
{
foreach (var nAndV in context.TemplatePlaceholderNameAndValues)
{
var name = nAndV.Name.Replace("{", "").Replace("}", "");
if (context.DownstreamRequest.Query.Contains(name) &&
context.DownstreamRequest.Query.Contains(nAndV.Value))
{
var questionMarkOrAmpersand = context.DownstreamRequest.Query.IndexOf(name, StringComparison.Ordinal);
context.DownstreamRequest.Query = context.DownstreamRequest.Query.Remove(questionMarkOrAmpersand - 1, 1);
var rgx = new Regex($@"\b{name}={nAndV.Value}\b");
context.DownstreamRequest.Query = rgx.Replace(context.DownstreamRequest.Query, "");
if (!string.IsNullOrEmpty(context.DownstreamRequest.Query))
{
context.DownstreamRequest.Query = '?' + context.DownstreamRequest.Query.Substring(1);
}
}
}
}
private string GetPath(DownstreamPath dsPath)
{
return dsPath.Value.Substring(0, dsPath.Value.IndexOf("?"));
return dsPath.Value.Substring(0, dsPath.Value.IndexOf("?", StringComparison.Ordinal));
}
private string GetQueryString(DownstreamPath dsPath)
{
return dsPath.Value.Substring(dsPath.Value.IndexOf("?"));
return dsPath.Value.Substring(dsPath.Value.IndexOf("?", StringComparison.Ordinal));
}
private bool ContainsQueryString(DownstreamPath dsPath)

View File

@ -2,13 +2,12 @@ namespace Ocelot.Request.Creator
{
using System.Net.Http;
using Ocelot.Request.Middleware;
using System.Runtime.InteropServices;
using Ocelot.Infrastructure;
public class DownstreamRequestCreator : IDownstreamRequestCreator
{
private readonly IFrameworkDescription _framework;
private const string dotNetFramework = ".NET Framework";
private const string DotNetFramework = ".NET Framework";
public DownstreamRequestCreator(IFrameworkDescription framework)
{
@ -24,8 +23,7 @@ namespace Ocelot.Request.Creator
* And MS HttpClient in Full Framework actually rejects it.
* see #366 issue
**/
if(_framework.Get().Contains(dotNetFramework))
if(_framework.Get().Contains(DotNetFramework))
{
if (request.Method == HttpMethod.Get ||
request.Method == HttpMethod.Head ||

View File

@ -28,6 +28,7 @@ namespace Ocelot.Request.Middleware
public async Task Invoke(DownstreamContext context)
{
var downstreamRequest = await _requestMapper.Map(context.HttpContext.Request);
if (downstreamRequest.IsError)
{
SetPipelineError(context, downstreamRequest.Errors);

View File

@ -2,14 +2,16 @@ namespace Ocelot.Values
{
public class UpstreamPathTemplate
{
public UpstreamPathTemplate(string template, int priority)
public UpstreamPathTemplate(string template, int priority, bool containsQueryString)
{
Template = template;
Priority = priority;
ContainsQueryString = containsQueryString;
}
public string Template { get; }
public int Priority { get; }
public int Priority { get; }
public bool ContainsQueryString { get; }
}
}