From ff5776613f973e838473eedb7f3eeac988da25f0 Mon Sep 17 00:00:00 2001 From: TomPallister Date: Sun, 6 Nov 2016 11:50:33 +0000 Subject: [PATCH] Now defaults to case insensitive routing but you can override with a setting, also global request id setting available --- README.md | 31 ++- configuration-explanation.txt | 146 +++++++------ .../Creator/FileOcelotConfigurationCreator.cs | 83 +++++--- .../Configuration/File/FileConfiguration.cs | 2 + .../File/FileGlobalConfiguration.cs | 7 + src/Ocelot/Configuration/File/FileReRoute.cs | 1 + .../CaseSensitiveRoutingTests.cs | 200 ++++++++++++++++++ test/Ocelot.AcceptanceTests/RequestIdTests.cs | 30 +++ .../Ocelot.AcceptanceTests/configuration.json | 2 +- .../FileConfigurationCreatorTests.cs | 138 +++++++++++- .../UrlMatcher/RegExUrlMatcherTests.cs | 21 ++ 11 files changed, 552 insertions(+), 109 deletions(-) create mode 100644 src/Ocelot/Configuration/File/FileGlobalConfiguration.cs create mode 100644 test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs diff --git a/README.md b/README.md index 21d760a2..3a31d383 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,18 @@ All versions can be found [here](https://www.nuget.org/packages/Ocelot/) An example configuration can be found [here](https://github.com/TomPallister/Ocelot/blob/develop/test/Ocelot.ManualTest/configuration.json) and an explained configuration can be found [here](https://github.com/TomPallister/Ocelot/blob/develop/configuration-explanation.txt). More detailed instructions to come on how to configure this. +There are two sections to the configuration. An array of ReRoutes and a GlobalConfiguration. +The ReRoutes are the objects that tell Ocelot how to treat an upstream request. The Global +configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful +if you don't want to manage lots of ReRoute specific settings. + + { + "ReRoutes": [], + "GlobalConfiguration": {} + } + +More information on how to use these options is below.. + ## Startup An example startup using a json file for configuration can be seen below. @@ -125,8 +137,14 @@ The placeholder needs to be in both the DownstreamTemplate and UpstreamTemplate. Ocelot will attempt to replace the placeholder with the correct variable value from the Upstream URL when the request comes in. -At the moment all Ocelot routing is case sensitive. I think I will turn this off by default -in the future with an options to make Ocelot case sensitive per ReRoute. +At the moment without any configuration Ocelot will default to all ReRoutes being case insensitive. +In order to change this you can specify on a per ReRoute basis the following setting. + + "ReRouteIsCaseSensitive": true + +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 +the ReRoute to be case sensitive is my advice! ## Authentication @@ -263,6 +281,13 @@ In order to use the requestid feature in your ReRoute configuration add this set In this example OcRequestId is the request header that contains the clients request id. +There is also a setting in the GlobalConfiguration section which will override whatever has been +set at ReRoute level for the request id. The setting is as fllows. + + "RequestIdKey": "OcRequestId", + +It behaves in exactly the same way as the ReRoute level RequestIdKey settings. + ## Caching Ocelot supports some very rudimentary caching at the moment provider by @@ -300,6 +325,8 @@ forwarded to the downstream service. Obviously this would break everything :( and doesnt check the response is OK. I think the fact you can even call stuff that isnt available is annoying. Let alone it be null. ++ The Ocelot Request Id starts getting logged too late in the pipeline. + ## Coming up You can see what we are working on [here](https://github.com/TomPallister/Ocelot/projects/1) diff --git a/configuration-explanation.txt b/configuration-explanation.txt index 1256a562..c2ae4864 100644 --- a/configuration-explanation.txt +++ b/configuration-explanation.txt @@ -1,70 +1,80 @@ -"ReRoutes": [ - { - # The url we are forwarding the request to - "UpstreamTemplate": "/identityserverexample", - # The path we are listening on for this re route - "UpstreamTemplate": "/identityserverexample", - # The method we are listening for on this re route - "UpstreamHttpMethod": "Get", - # Only support identity server at the moment - "AuthenticationOptions": { - "Provider": "IdentityServer", - "ProviderRootUrl": "http://localhost:52888", - "ScopeName": "api", - "AdditionalScopes": [ - "openid", - "offline_access" - ], - # Required if using reference tokens - "ScopeSecret": "secret" +{ + "ReRoutes": [ + { + # The url we are forwarding the request to + "UpstreamTemplate": "/identityserverexample", + # The path we are listening on for this re route + "UpstreamTemplate": "/identityserverexample", + # The method we are listening for on this re route + "UpstreamHttpMethod": "Get", + # Only support identity server at the moment + "AuthenticationOptions": { + "Provider": "IdentityServer", + "ProviderRootUrl": "http://localhost:52888", + "ScopeName": "api", + "AdditionalScopes": [ + "openid", + "offline_access" + ], + # Required if using reference tokens + "ScopeSecret": "secret" + }, + # WARNING - will overwrite any headers already in the request with these values. + # Ocelot will look in the user claims for the key in [] then return the value and save + # it as a header with the given key before the colon (:). The index selection on value + # means that Ocelot will use the delimiter specified after the next > to split the + # claim value and return the index specified. + "AddHeadersToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + # WARNING - will overwrite any claims already in the request with these values. + # Ocelot will look in the user claims for the key in [] then return the value and save + # it as a claim with the given key before the colon (:). The index selection on value + # means that Ocelot will use the delimiter specified after the next > to split the + # claim value and return the index specified. + "AddClaimsToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + # WARNING - will overwrite any query string entries already in the request with these values. + # Ocelot will look in the user claims for the key in [] then return the value and save + # it as a query string with the given key before the colon (:). The index selection on value + # means that Ocelot will use the delimiter specified after the next > to split the + # claim value and return the index specified. + "AddQueriesToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + # This specifies any claims that are required for the user to access this re route. + # In this example the user must have the claim type UserType and + # the value must be registered + "RouteClaimsRequirement": { + "UserType": "registered" + }, + # This tells Ocelot to look for a header and use its value as a request/correlation id. + # If it is set here then the id will be forwarded to the downstream service. If it + # does not then it will not be forwarded + "RequestIdKey": "OcRequestId", + # If this is set the response from the downstream service will be cached using the key that called it. + # This gives the user a chance to influence the key by adding some random query string paramter for + # a user id or something that would get ignored by the downstream service. This is a hack and I + # intend to provide a mechanism the user can specify for the ttl caching. Also want to expand + # the caching a lot. + "FileCacheOptions": { "TtlSeconds": 15 }, + # The value of this is used when matching the upstream template to an upstream url. + "ReRouteIsCaseSensitive": false }, - # WARNING - will overwrite any headers already in the request with these values. - # Ocelot will look in the user claims for the key in [] then return the value and save - # it as a header with the given key before the colon (:). The index selection on value - # means that Ocelot will use the delimiter specified after the next > to split the - # claim value and return the index specified. - "AddHeadersToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - # WARNING - will overwrite any claims already in the request with these values. - # Ocelot will look in the user claims for the key in [] then return the value and save - # it as a claim with the given key before the colon (:). The index selection on value - # means that Ocelot will use the delimiter specified after the next > to split the - # claim value and return the index specified. - "AddClaimsToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - # WARNING - will overwrite any query string entries already in the request with these values. - # Ocelot will look in the user claims for the key in [] then return the value and save - # it as a query string with the given key before the colon (:). The index selection on value - # means that Ocelot will use the delimiter specified after the next > to split the - # claim value and return the index specified. - "AddQueriesToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - # This specifies any claims that are required for the user to access this re route. - # In this example the user must have the claim type UserType and - # the value must be registered - "RouteClaimsRequirement": { - "UserType": "registered" - }, - # This tells Ocelot to look for a header and use its value as a request/correlation id. - # If it is set here then the id will be forwarded to the downstream service. If it - # does not then it will not be forwarded - "RequestIdKey": "OcRequestId", - # If this is set the response from the downstream service will be cached using the key that called it. - # This gives the user a chance to influence the key by adding some random query string paramter for - # a user id or something that would get ignored by the downstream service. This is a hack and I - # intend to provide a mechanism the user can specify for the ttl caching. Also want to expand - # the caching a lot. - "FileCacheOptions": { "TtlSeconds": 15 } + # This section is meant to be for global configuration settings + "GlobalConfiguration": { + # If this is set it will override any route specific request id keys, behaves the same + # otherwise + "RequestIdKey": "OcRequestId", + } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index b777daca..51578756 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -19,6 +19,8 @@ namespace Ocelot.Configuration.Creator private readonly IConfigurationValidator _configurationValidator; private const string RegExMatchEverything = ".*"; private const string RegExMatchEndString = "$"; + private const string RegExIgnoreCase = "(?i)"; + private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; private readonly ILogger _logger; @@ -65,14 +67,56 @@ namespace Ocelot.Configuration.Creator foreach (var reRoute in _options.Value.ReRoutes) { - var ocelotReRoute = SetUpReRoute(reRoute); + var ocelotReRoute = SetUpReRoute(reRoute, _options.Value.GlobalConfiguration); reRoutes.Add(ocelotReRoute); } return new OcelotConfiguration(reRoutes); } - private ReRoute SetUpReRoute(FileReRoute reRoute) + private ReRoute SetUpReRoute(FileReRoute reRoute, FileGlobalConfiguration globalConfiguration) + { + var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey); + + var upstreamTemplate = BuildUpstreamTemplate(reRoute); + + var isAuthenticated = !string.IsNullOrEmpty(reRoute.AuthenticationOptions?.Provider); + + var isAuthorised = reRoute.RouteClaimsRequirement?.Count > 0; + + var isCached = reRoute.FileCacheOptions.TtlSeconds > 0; + + var requestIdKey = globalRequestIdConfiguration + ? globalConfiguration.RequestIdKey + : reRoute.RequestIdKey; + + + if (isAuthenticated) + { + var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider, + reRoute.AuthenticationOptions.ProviderRootUrl, reRoute.AuthenticationOptions.ScopeName, + reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes, + reRoute.AuthenticationOptions.ScopeSecret); + + var claimsToHeaders = GetAddThingsToRequest(reRoute.AddHeadersToRequest); + var claimsToClaims = GetAddThingsToRequest(reRoute.AddClaimsToRequest); + var claimsToQueries = GetAddThingsToRequest(reRoute.AddQueriesToRequest); + + return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, + reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, + authOptionsForRoute, claimsToHeaders, claimsToClaims, + reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries, + requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds)); + } + + return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, + reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, + null, new List(), new List(), + reRoute.RouteClaimsRequirement, isAuthorised, new List(), + requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds)); + } + + private string BuildUpstreamTemplate(FileReRoute reRoute) { var upstreamTemplate = reRoute.UpstreamTemplate; @@ -94,38 +138,9 @@ namespace Ocelot.Configuration.Creator upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything); } - upstreamTemplate = $"{upstreamTemplate}{RegExMatchEndString}"; - - var isAuthenticated = !string.IsNullOrEmpty(reRoute.AuthenticationOptions?.Provider); - - var isAuthorised = reRoute.RouteClaimsRequirement?.Count > 0; - - var isCached = reRoute.FileCacheOptions.TtlSeconds > 0; - - if (isAuthenticated) - { - var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider, - reRoute.AuthenticationOptions.ProviderRootUrl, reRoute.AuthenticationOptions.ScopeName, - reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes, - reRoute.AuthenticationOptions.ScopeSecret); - - var claimsToHeaders = GetAddThingsToRequest(reRoute.AddHeadersToRequest); - var claimsToClaims = GetAddThingsToRequest(reRoute.AddClaimsToRequest); - var claimsToQueries = GetAddThingsToRequest(reRoute.AddQueriesToRequest); - - return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, - reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, - authOptionsForRoute, claimsToHeaders, claimsToClaims, - reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries, - reRoute.RequestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds) - ); - } - - return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, - reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, - null, new List(), new List(), - reRoute.RouteClaimsRequirement, isAuthorised, new List(), - reRoute.RequestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds)); + return reRoute.ReRouteIsCaseSensitive + ? $"{upstreamTemplate}{RegExMatchEndString}" + : $"{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; } private List GetAddThingsToRequest(Dictionary thingBeingAdded) diff --git a/src/Ocelot/Configuration/File/FileConfiguration.cs b/src/Ocelot/Configuration/File/FileConfiguration.cs index d82fb94e..18938a0e 100644 --- a/src/Ocelot/Configuration/File/FileConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileConfiguration.cs @@ -7,8 +7,10 @@ namespace Ocelot.Configuration.File public FileConfiguration() { ReRoutes = new List(); + GlobalConfiguration = new FileGlobalConfiguration(); } public List ReRoutes { get; set; } + public FileGlobalConfiguration GlobalConfiguration { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs new file mode 100644 index 00000000..07c17188 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Configuration.File +{ + public class FileGlobalConfiguration + { + public string RequestIdKey { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index 3547cf99..3773dd9d 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -24,5 +24,6 @@ namespace Ocelot.Configuration.File public Dictionary AddQueriesToRequest { get; set; } public string RequestIdKey { get; set; } public FileCacheOptions FileCacheOptions { get; set; } + public bool ReRouteIsCaseSensitive { get; set; } } } \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs new file mode 100644 index 00000000..81a12381 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class CaseSensitiveRoutingTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + + public CaseSensitiveRoutingTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_when_global_ignore_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://localhost:51879/api/products/{productId}", + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get" + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_reroute_ignore_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://localhost:51879/api/products/{productId}", + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = false + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_404_when_reroute_respect_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://localhost:51879/api/products/{productId}", + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_reroute_respect_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://localhost:51879/api/products/{productId}", + UpstreamTemplate = "/PRODUCTS/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_404_when_global_respect_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://localhost:51879/api/products/{productId}", + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_global_respect_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://localhost:51879/api/products/{productId}", + UpstreamTemplate = "/PRODUCTS/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/RequestIdTests.cs b/test/Ocelot.AcceptanceTests/RequestIdTests.cs index 7a733f24..512736ae 100644 --- a/test/Ocelot.AcceptanceTests/RequestIdTests.cs +++ b/test/Ocelot.AcceptanceTests/RequestIdTests.cs @@ -76,6 +76,36 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_use_global_request_id_and_forward() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://localhost:51879/", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + RequestIdKey = _steps.RequestIdKey + } + }; + + var requestId = Guid.NewGuid().ToString(); + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", requestId)) + .Then(x => _steps.ThenTheRequestIdIsReturned(requestId)) + .BDDfy(); + } + private void GivenThereIsAServiceRunningOn(string url) { _builder = new WebHostBuilder() diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json index d6177a81..984f851c 100644 --- a/test/Ocelot.AcceptanceTests/configuration.json +++ b/test/Ocelot.AcceptanceTests/configuration.json @@ -1 +1 @@ -{"ReRoutes":[{"DownstreamTemplate":"http://localhost:41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0}}]} \ No newline at end of file +{"ReRoutes":[{"DownstreamTemplate":"http://localhost:41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false}],"GlobalConfiguration":{"RequestIdKey":null}} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 8d0c6092..ba1b7569 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -36,7 +36,37 @@ namespace Ocelot.UnitTests.Configuration } [Fact] - public void should_create_template_pattern_that_matches_anything_to_end_of_string() + public void should_use_reroute_case_sensitivity_value() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = false + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_set_upstream_template_pattern_to_ignore_case_sensitivity() { this.Given(x => x.GivenTheConfigIs(new FileConfiguration { @@ -49,6 +79,101 @@ namespace Ocelot.UnitTests.Configuration UpstreamHttpMethod = "Get" } } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_set_upstream_template_pattern_to_respect_case_sensitivity() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("/api/products/.*$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_set_global_request_id_key() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + RequestIdKey = "blahhhh" + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("/api/products/.*$") + .WithRequestIdKey("blahhhh") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_anything_to_end_of_string() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } })) .And(x => x.GivenTheConfigIsValid()) .When(x => x.WhenICreateTheConfig()) @@ -95,6 +220,7 @@ namespace Ocelot.UnitTests.Configuration UpstreamTemplate = "/api/products/{productId}", DownstreamTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true, AuthenticationOptions = new FileAuthenticationOptions { AdditionalScopes = new List(), @@ -153,6 +279,7 @@ namespace Ocelot.UnitTests.Configuration UpstreamTemplate = "/api/products/{productId}", DownstreamTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true, AuthenticationOptions = new FileAuthenticationOptions { AdditionalScopes = new List(), @@ -183,7 +310,8 @@ namespace Ocelot.UnitTests.Configuration { UpstreamTemplate = "/api/products/{productId}/variants/{variantId}", DownstreamTemplate = "/products/{productId}", - UpstreamHttpMethod = "Get" + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true } } })) @@ -212,7 +340,8 @@ namespace Ocelot.UnitTests.Configuration { UpstreamTemplate = "/api/products/{productId}/variants/{variantId}/", DownstreamTemplate = "/products/{productId}", - UpstreamHttpMethod = "Get" + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true } } })) @@ -241,7 +370,8 @@ namespace Ocelot.UnitTests.Configuration { UpstreamTemplate = "/", DownstreamTemplate = "/api/products/", - UpstreamHttpMethod = "Get" + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true } } })) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs index 02e8eb91..dc94288b 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -128,6 +128,27 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher .BDDfy(); } + [Fact] + public void should_ignore_case_sensitivity() + { + this.Given(x => x.GivenIHaveAUpstreamPath("API/product/products/1/categories/2/variant/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("(?i)api/product/products/.*/categories/.*/variant/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_respect_case_sensitivity() + { + this.Given(x => x.GivenIHaveAUpstreamPath("API/product/products/1/categories/2/variant/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api/product/products/.*/categories/.*/variant/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + private void GivenIHaveAUpstreamPath(string downstreamPath) { _downstreamUrlPath = downstreamPath;