Skip to content

Commit

Permalink
Added support for query string parameters in upstream path template (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
TomPallister authored Jul 12, 2018
1 parent 19ea93d commit 8f4ae03
Show file tree
Hide file tree
Showing 24 changed files with 664 additions and 143 deletions.
31 changes: 30 additions & 1 deletion docs/features/routing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,33 @@ Ocelot allow's you to specify a querystring as part of the DownstreamPathTemplat
}
}
In this example Ocelot will use the value from the {unitId} in the upstream path template and add it to the downstream request as a query string parameter called unitId! Please note you cannot use query string parameters to match routes in the UpstreamPathTemplate.
In this example Ocelot will use the value from the {unitId} in the upstream path template and add it to the downstream request as a query string parameter called unitId!

Ocelot will also allow you to put query string parametrs in the UpstreamPathTemplate so you can match certain queries to certain services.

.. code-block:: json
{
"ReRoutes": [
{
"DownstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates",
"UpstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}",
"UpstreamHttpMethod": [
"Get"
],
"DownstreamScheme": "http",
"DownstreamHostAndPorts": [
{
"Host": "localhost",
"Port": 50110
}
]
}
],
"GlobalConfiguration": {
"UseServiceDiscovery": false
}
}
In this example Ocelot will only match requests that have a matching url path and the querystring starts with unitId=something. You can have other queries after this
but you must start with the matching parameter. Also in this example Ocelot will swap the unitId param from the query string and use it in the downstream request path.
17 changes: 13 additions & 4 deletions src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public class UpstreamTemplatePatternCreator : IUpstreamTemplatePatternCreator

public UpstreamPathTemplate Create(IReRoute reRoute)
{
var upstreamTemplate = reRoute.UpstreamPathTemplate;
var upstreamTemplate = reRoute.UpstreamPathTemplate;


var placeholders = new List<string>();

Expand All @@ -30,9 +31,17 @@ public UpstreamPathTemplate Create(IReRoute reRoute)
//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)
Expand All @@ -42,7 +51,7 @@ public UpstreamPathTemplate Create(IReRoute reRoute)

if (upstreamTemplate == "/")
{
return new UpstreamPathTemplate(RegExForwardSlashOnly, reRoute.Priority);
return new UpstreamPathTemplate(RegExForwardSlashOnly, reRoute.Priority, containsQueryString);
}

if(upstreamTemplate.EndsWith("/"))
Expand All @@ -54,7 +63,7 @@ public UpstreamPathTemplate Create(IReRoute reRoute)
? $"^{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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public DownstreamRouteCreator(IQoSOptionsCreator qoSOptionsCreator)
_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);

Expand Down
12 changes: 6 additions & 6 deletions src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public DownstreamRouteFinder(IUrlPathToUrlTemplateMatcher urlMatcher, IPlacehold
_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>();

Expand All @@ -28,11 +28,11 @@ public Response<DownstreamRoute> Get(string path, string httpMethod, IInternalCo

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));
}
}

Expand All @@ -44,7 +44,7 @@ public Response<DownstreamRoute> Get(string path, string httpMethod, IInternalCo
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)
Expand All @@ -53,9 +53,9 @@ private bool RouteIsApplicableToThisRequest(ReRoute reRoute, string httpMethod,
(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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@ public async Task Invoke(DownstreamContext context)
{
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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
11 changes: 9 additions & 2 deletions src/Ocelot/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -44,7 +63,7 @@ public Response<List<PlaceholderNameAndValue>> Find(string path, string pathTemp
}
else
{
var placeholderValue = GetPlaceholderValue(pathTemplate, placeholderName, path, counterForPath + 1);
var placeholderValue = GetPlaceholderValue(pathTemplate, query, placeholderName, path, counterForPath + 1, '/');
placeHolderNameAndValues.Add(new PlaceholderNameAndValue(placeholderName, placeholderValue));
}

Expand All @@ -57,6 +76,21 @@ public Response<List<PlaceholderNameAndValue>> Find(string path, string pathTemp
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
Expand All @@ -69,11 +103,11 @@ private bool NothingAfterFirstForwardSlash(string path)
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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -55,12 +53,11 @@ public async Task Invoke(DownstreamContext context)
{
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;
}
}
Expand All @@ -70,14 +67,37 @@ public async Task Invoke(DownstreamContext context)
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)
Expand Down
6 changes: 2 additions & 4 deletions src/Ocelot/Request/Creator/DownstreamRequestCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -24,8 +23,7 @@ public DownstreamRequest Create(HttpRequestMessage request)
* 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 ||
Expand Down
Loading

0 comments on commit 8f4ae03

Please sign in to comment.