Changed routing to support a catch all style

This commit is contained in:
Tom Gardham-Pallister 2018-01-04 21:35:44 +00:00
parent 931a115ffa
commit 9cb201cfa9
6 changed files with 269 additions and 14 deletions

View File

@ -63,3 +63,36 @@ In order to change this you can specify on a per ReRoute basis the following set
This means that when Ocelot tries to match the incoming upstream url with an upstream template the This means that when Ocelot tries to match the incoming upstream url with an upstream template the
evaluation will be case sensitive. This setting defaults to false so only set it if you want evaluation will be case sensitive. This setting defaults to false so only set it if you want
the ReRoute to be case sensitive is my advice! the ReRoute to be case sensitive is my advice!
Catch All
^^^^^^^^^
Ocelot's routing also supports a catch all style routing where the user can specify that they want to match all traffic if you set up your config like below the request will be proxied straight through (it doesnt have to be url any placeholder name will work).
.. code-block:: json
{
"DownstreamPathTemplate": "/{url}",
"DownstreamScheme": "https",
"DownstreamPort": 80,
"DownstreamHost" "localhost",
"UpstreamPathTemplate": "/{url}",
"UpstreamHttpMethod": [ "Get" ]
}
There is a gotcha if you want to do this kind of thing. The order of the ReRoutes in the config will now matter.
If you had this ReRoute after the catch all then it would never be matched. However if it was before the catch all it would match first.
.. code-block:: json
{
"DownstreamPathTemplate": "/",
"DownstreamScheme": "https",
"DownstreamPort": 80,
"DownstreamHost" "10.0.10.1",
"UpstreamPathTemplate": "/",
"UpstreamHttpMethod": [ "Get" ]
}
This is because when Ocelot tries to match a request to a ReRoute it has to look at all the possible matches and uses a regular expression to test the url.

View File

@ -26,6 +26,7 @@ namespace Ocelot.Configuration.Creator
var placeHolderName = upstreamTemplate.Substring(i, difference); var placeHolderName = upstreamTemplate.Substring(i, difference);
placeholders.Add(placeHolderName); placeholders.Add(placeHolderName);
//hack to handle /{url} case
if(ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket)) if(ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket))
{ {
return RegExForwardSlashAndOnePlaceHolder; return RegExForwardSlashAndOnePlaceHolder;

View File

@ -34,6 +34,30 @@ namespace Ocelot.DownstreamRouteFinder.UrlMatcher
return new OkResponse<List<UrlPathPlaceholderNameAndValue>>(templateKeysAndValues); return new OkResponse<List<UrlPathPlaceholderNameAndValue>>(templateKeysAndValues);
} }
//hacking to handle special case of /{url}
//if this char is a forward slash and the template starts with /{ and last character of string is the next }
else if(string.IsNullOrEmpty(upstreamUrlPath) || (upstreamUrlPath.Length > counterForUrl && upstreamUrlPath[counterForUrl] == '/') && upstreamUrlPathTemplate.Length > 1
&& upstreamUrlPathTemplate.Substring(0, 2) == "/{"
&& upstreamUrlPathTemplate.IndexOf('}') == upstreamUrlPathTemplate.Length - 1)
{
var endOfPlaceholder = GetNextCounterPosition(upstreamUrlPathTemplate, counterForTemplate, '}');
var variableName = GetPlaceholderVariableName(upstreamUrlPathTemplate, 1);
UrlPathPlaceholderNameAndValue templateVariableNameAndValue;
if(upstreamUrlPath.Length == 1 || upstreamUrlPath.Length == 0)
{
templateVariableNameAndValue = new UrlPathPlaceholderNameAndValue(variableName, "");
}
else
{
var variableValue = GetPlaceholderVariableValue(upstreamUrlPathTemplate, variableName, upstreamUrlPath, counterForUrl + 1);
templateVariableNameAndValue = new UrlPathPlaceholderNameAndValue(variableName, variableValue);
}
templateKeysAndValues.Add(templateVariableNameAndValue);
counterForTemplate = endOfPlaceholder;
}
counterForUrl++; counterForUrl++;
} }

View File

@ -52,7 +52,7 @@ namespace Ocelot.AcceptanceTests
} }
}; };
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 200, "Hello from Laura")) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/", "/", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
@ -61,6 +61,108 @@ namespace Ocelot.AcceptanceTests
.BDDfy(); .BDDfy();
} }
[Fact]
public void should_return_response_200_favouring_forward_slash_with_path_route()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/{url}",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 51880,
UpstreamPathTemplate = "/{url}",
UpstreamHttpMethod = new List<string> { "Get" },
},
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 51879,
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" },
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", "/test", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/test"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.BDDfy();
}
[Fact]
public void should_return_response_200_favouring_forward_slash_route_because_it_is_first()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 51880,
UpstreamPathTemplate = "/",
UpstreamHttpMethod = new List<string> { "Get" },
},
new FileReRoute
{
DownstreamPathTemplate = "/{url}",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 51879,
UpstreamPathTemplate = "/{url}",
UpstreamHttpMethod = new List<string> { "Get" },
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", "/", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.BDDfy();
}
[Fact]
public void should_return_response_200_with_nothing_and_placeholder_only()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamPathTemplate = "/{url}",
DownstreamScheme = "http",
DownstreamHost = "localhost",
DownstreamPort = 51879,
UpstreamPathTemplate = "/{url}",
UpstreamHttpMethod = new List<string> { "Get" },
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway(""))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.BDDfy();
}
[Fact] [Fact]
public void should_return_response_200_with_simple_url() public void should_return_response_200_with_simple_url()
{ {
@ -80,7 +182,7 @@ namespace Ocelot.AcceptanceTests
} }
}; };
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 200, "Hello from Laura")) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
@ -378,7 +480,7 @@ namespace Ocelot.AcceptanceTests
} }
}; };
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 201, string.Empty)) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenOcelotIsRunning())
.And(x => _steps.GivenThePostHasContent("postContent")) .And(x => _steps.GivenThePostHasContent("postContent"))
@ -406,7 +508,7 @@ namespace Ocelot.AcceptanceTests
} }
}; };
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 200, "Hello from Laura")) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/newThing", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/newThing?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/newThing?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-"))
@ -434,7 +536,7 @@ namespace Ocelot.AcceptanceTests
} }
}; };
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/myApp1Name/api/products/1", 200, "Some Product")) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/api/products/1", 200, "Some Product"))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/myApp1Name/api/products/1")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/myApp1Name/api/products/1"))
@ -490,7 +592,7 @@ namespace Ocelot.AcceptanceTests
} }
}; };
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "", 200, "Hello from Laura")) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
@ -564,7 +666,7 @@ namespace Ocelot.AcceptanceTests
} }
}; };
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", "", 200, "Hello from Laura")) this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51899", "/api/swagger/lib/backbone-min.js", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/platform/swagger/lib/backbone-min.js")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/platform/swagger/lib/backbone-min.js"))
@ -587,8 +689,17 @@ namespace Ocelot.AcceptanceTests
app.Run(async context => app.Run(async context =>
{ {
_downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value;
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(responseBody); if(_downstreamPath != basePath)
{
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync("downstream path didnt match base path");
}
else
{
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(responseBody);
}
}); });
}) })
.Build(); .Build();

View File

@ -18,6 +18,18 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher
_urlMatcher = new RegExUrlMatcher(); _urlMatcher = new RegExUrlMatcher();
} }
[Fact]
public void should_not_match_slash_becaue_we_need_to_match_something_after_it()
{
const string RegExForwardSlashAndOnePlaceHolder = "^/[0-9a-zA-Z].*";
this.Given(x => x.GivenIHaveAUpstreamPath("/"))
.And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern(RegExForwardSlashAndOnePlaceHolder))
.When(x => x.WhenIMatchThePaths())
.And(x => x.ThenTheResultIsFalse())
.BDDfy();
}
[Fact] [Fact]
public void should_not_match_forward_slash_only_regex() public void should_not_match_forward_slash_only_regex()
{ {

View File

@ -8,14 +8,14 @@ using Xunit;
namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher
{ {
public class UrlPathToUrlTemplateMatcherTests public class UrlPathPlaceholderNameAndValueFinderTests
{ {
private readonly IUrlPathPlaceholderNameAndValueFinder _finder; private readonly IUrlPathPlaceholderNameAndValueFinder _finder;
private string _downstreamUrlPath; private string _downstreamUrlPath;
private string _downstreamPathTemplate; private string _downstreamPathTemplate;
private Response<List<UrlPathPlaceholderNameAndValue>> _result; private Response<List<UrlPathPlaceholderNameAndValue>> _result;
public UrlPathToUrlTemplateMatcherTests() public UrlPathPlaceholderNameAndValueFinderTests()
{ {
_finder = new UrlPathPlaceholderNameAndValueFinder(); _finder = new UrlPathPlaceholderNameAndValueFinder();
} }
@ -30,6 +30,81 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher
.BDDfy(); .BDDfy();
} }
[Fact]
public void can_match_down_stream_url_with_nothing_then_placeholder_no_value_is_blank()
{
var expectedTemplates = new List<UrlPathPlaceholderNameAndValue>
{
new UrlPathPlaceholderNameAndValue("{url}", "")
};
this.Given(x => x.GivenIHaveAUpstreamPath(""))
.And(x => x.GivenIHaveAnUpstreamUrlTemplate("/{url}"))
.When(x => x.WhenIFindTheUrlVariableNamesAndValues())
.And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates))
.BDDfy();
}
[Fact]
public void can_match_down_stream_url_with_nothing_then_placeholder_value_is_test()
{
var expectedTemplates = new List<UrlPathPlaceholderNameAndValue>
{
new UrlPathPlaceholderNameAndValue("{url}", "test")
};
this.Given(x => x.GivenIHaveAUpstreamPath("/test"))
.And(x => x.GivenIHaveAnUpstreamUrlTemplate("/{url}"))
.When(x => x.WhenIFindTheUrlVariableNamesAndValues())
.And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates))
.BDDfy();
}
[Fact]
public void can_match_down_stream_url_with_forward_slash_then_placeholder_no_value_is_blank()
{
var expectedTemplates = new List<UrlPathPlaceholderNameAndValue>
{
new UrlPathPlaceholderNameAndValue("{url}", "")
};
this.Given(x => x.GivenIHaveAUpstreamPath("/"))
.And(x => x.GivenIHaveAnUpstreamUrlTemplate("/{url}"))
.When(x => x.WhenIFindTheUrlVariableNamesAndValues())
.And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates))
.BDDfy();
}
[Fact]
public void can_match_down_stream_url_with_forward_slash()
{
var expectedTemplates = new List<UrlPathPlaceholderNameAndValue>
{
};
this.Given(x => x.GivenIHaveAUpstreamPath("/"))
.And(x => x.GivenIHaveAnUpstreamUrlTemplate("/"))
.When(x => x.WhenIFindTheUrlVariableNamesAndValues())
.And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates))
.BDDfy();
}
[Fact]
public void can_match_down_stream_url_with_forward_slash_then_placeholder_then_another_value()
{
var expectedTemplates = new List<UrlPathPlaceholderNameAndValue>
{
new UrlPathPlaceholderNameAndValue("{url}", "1")
};
this.Given(x => x.GivenIHaveAUpstreamPath("/1/products"))
.And(x => x.GivenIHaveAnUpstreamUrlTemplate("/{url}/products"))
.When(x => x.WhenIFindTheUrlVariableNamesAndValues())
.And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates))
.BDDfy();
}
[Fact] [Fact]
public void should_not_find_anything() public void should_not_find_anything()
{ {
@ -169,8 +244,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher
{ {
foreach (var expectedResult in expectedResults) foreach (var expectedResult in expectedResults)
{ {
var result = _result.Data var result = _result.Data.First(t => t.TemplateVariableName == expectedResult.TemplateVariableName);
.First(t => t.TemplateVariableName == expectedResult.TemplateVariableName);
result.TemplateVariableValue.ShouldBe(expectedResult.TemplateVariableValue); result.TemplateVariableValue.ShouldBe(expectedResult.TemplateVariableValue);
} }
} }