diff --git a/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/DownstreamRoute.cs b/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/DownstreamRoute.cs new file mode 100644 index 00000000..b88f0063 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/DownstreamRoute.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Ocelot.Library.Infrastructure.UrlMatcher; + +namespace Ocelot.Library.Infrastructure.DownstreamRouteFinder +{ + public class DownstreamRoute + { + public DownstreamRoute(List templateVariableNameAndValues, string downstreamUrlTemplate) + { + TemplateVariableNameAndValues = templateVariableNameAndValues; + DownstreamUrlTemplate = downstreamUrlTemplate; + } + public List TemplateVariableNameAndValues { get; private set; } + public string DownstreamUrlTemplate { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/DownstreamRouteFinder.cs b/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/DownstreamRouteFinder.cs new file mode 100644 index 00000000..63ddfacd --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/DownstreamRouteFinder.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Options; +using Ocelot.Library.Infrastructure.Responses; +using Ocelot.Library.Infrastructure.UrlMatcher; + +namespace Ocelot.Library.Infrastructure.DownstreamRouteFinder +{ + public class DownstreamRouteFinder : IDownstreamRouteFinder + { + private readonly IOptions _configuration; + private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; + + public DownstreamRouteFinder(IOptions configuration, IUrlPathToUrlTemplateMatcher urlMatcher) + { + _configuration = configuration; + _urlMatcher = urlMatcher; + } + + public Response FindDownstreamRoute(string upstreamUrlPath) + { + + foreach (var template in _configuration.Value.ReRoutes) + { + var urlMatch = _urlMatcher.Match(upstreamUrlPath, template.UpstreamTemplate); + + if (urlMatch.Match) + { + return new OkResponse(new DownstreamRoute(urlMatch.TemplateVariableNameAndValues, template.DownstreamTemplate)); + } + } + + return new ErrorResponse(new List + { + new UnableToFindDownstreamRouteError() + }); + } + } +} \ No newline at end of file diff --git a/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/IDownstreamRouteFinder.cs b/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/IDownstreamRouteFinder.cs new file mode 100644 index 00000000..1394b3af --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/IDownstreamRouteFinder.cs @@ -0,0 +1,9 @@ +using Ocelot.Library.Infrastructure.Responses; + +namespace Ocelot.Library.Infrastructure.DownstreamRouteFinder +{ + public interface IDownstreamRouteFinder + { + Response FindDownstreamRoute(string upstreamUrlPath); + } +} diff --git a/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/UnableToFindDownstreamRouteError.cs b/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/UnableToFindDownstreamRouteError.cs new file mode 100644 index 00000000..62618951 --- /dev/null +++ b/src/Ocelot.Library/Infrastructure/DownstreamRouteFinder/UnableToFindDownstreamRouteError.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Library.Infrastructure.Responses; + +namespace Ocelot.Library.Infrastructure.DownstreamRouteFinder +{ + public class UnableToFindDownstreamRouteError : Error + { + public UnableToFindDownstreamRouteError() : base("UnableToFindDownstreamRouteError") + { + } + } +} diff --git a/src/Ocelot.Library/Infrastructure/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs b/src/Ocelot.Library/Infrastructure/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs index 35dc563c..204eb212 100644 --- a/src/Ocelot.Library/Infrastructure/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs +++ b/src/Ocelot.Library/Infrastructure/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs @@ -1,17 +1,17 @@ using System.Text; -using Ocelot.Library.Infrastructure.UrlMatcher; +using Ocelot.Library.Infrastructure.DownstreamRouteFinder; namespace Ocelot.Library.Infrastructure.UrlTemplateReplacer { public class DownstreamUrlTemplateVariableReplacer : IDownstreamUrlTemplateVariableReplacer { - public string ReplaceTemplateVariable(UrlMatch urlMatch) + public string ReplaceTemplateVariable(DownstreamRoute downstreamRoute) { var upstreamUrl = new StringBuilder(); - upstreamUrl.Append(urlMatch.DownstreamUrlTemplate); + upstreamUrl.Append(downstreamRoute.DownstreamUrlTemplate); - foreach (var templateVarAndValue in urlMatch.TemplateVariableNameAndValues) + foreach (var templateVarAndValue in downstreamRoute.TemplateVariableNameAndValues) { upstreamUrl.Replace(templateVarAndValue.TemplateVariableName, templateVarAndValue.TemplateVariableValue); } diff --git a/src/Ocelot.Library/Infrastructure/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs b/src/Ocelot.Library/Infrastructure/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs index 62eac8bc..bc50a8fa 100644 --- a/src/Ocelot.Library/Infrastructure/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs +++ b/src/Ocelot.Library/Infrastructure/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs @@ -1,9 +1,10 @@ +using Ocelot.Library.Infrastructure.DownstreamRouteFinder; using Ocelot.Library.Infrastructure.UrlMatcher; namespace Ocelot.Library.Infrastructure.UrlTemplateReplacer { public interface IDownstreamUrlTemplateVariableReplacer { - string ReplaceTemplateVariable(UrlMatch urlMatch); + string ReplaceTemplateVariable(DownstreamRoute downstreamRoute); } } \ No newline at end of file diff --git a/src/Ocelot.Library/Middleware/ProxyMiddleware.cs b/src/Ocelot.Library/Middleware/ProxyMiddleware.cs index e723392e..1dd90dc6 100644 --- a/src/Ocelot.Library/Middleware/ProxyMiddleware.cs +++ b/src/Ocelot.Library/Middleware/ProxyMiddleware.cs @@ -1,56 +1,62 @@ +using System.Net; +using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Ocelot.Library.Infrastructure.UrlMatcher; +using Microsoft.Extensions.Options; +using Ocelot.Library.Infrastructure.Configuration; +using Ocelot.Library.Infrastructure.DownstreamRouteFinder; using Ocelot.Library.Infrastructure.UrlTemplateReplacer; namespace Ocelot.Library.Middleware { - using System.Net; - using Infrastructure.Configuration; - using Microsoft.Extensions.Options; - public class ProxyMiddleware { private readonly RequestDelegate _next; - private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; private readonly IDownstreamUrlTemplateVariableReplacer _urlReplacer; private readonly IOptions _configuration; + private readonly IDownstreamRouteFinder _downstreamRouteFinder; public ProxyMiddleware(RequestDelegate next, - IUrlPathToUrlTemplateMatcher urlMatcher, - IDownstreamUrlTemplateVariableReplacer urlReplacer, IOptions configuration) + IDownstreamUrlTemplateVariableReplacer urlReplacer, + IOptions configuration, + IDownstreamRouteFinder downstreamRouteFinder) { _next = next; - _urlMatcher = urlMatcher; _urlReplacer = urlReplacer; _configuration = configuration; + _downstreamRouteFinder = downstreamRouteFinder; } public async Task Invoke(HttpContext context) { var upstreamUrlPath = context.Request.Path.ToString(); - UrlMatch urlMatch = null; + var downstreamRoute = _downstreamRouteFinder.FindDownstreamRoute(upstreamUrlPath); - foreach (var template in _configuration.Value.ReRoutes) - { - urlMatch = _urlMatcher.Match(upstreamUrlPath, template.UpstreamTemplate); - - if (urlMatch.Match) - { - break; - } - } - - if (urlMatch == null || !urlMatch.Match) + if (downstreamRoute.IsError) { context.Response.StatusCode = (int)HttpStatusCode.NotFound; return; } - - var downstreamUrl = _urlReplacer.ReplaceTemplateVariable(urlMatch); + + var downstreamUrl = _urlReplacer.ReplaceTemplateVariable(downstreamRoute.Data); //make a http request to this endpoint...maybe bring in a library + using (var httpClient = new HttpClient()) + { + var httpMethod = new HttpMethod(context.Request.Method); + + var httpRequestMessage = new HttpRequestMessage(httpMethod, downstreamUrl); + + var response = await httpClient.SendAsync(httpRequestMessage); + + if (!response.IsSuccessStatusCode) + { + context.Response.StatusCode = (int)response.StatusCode; + return; + } + await context.Response.WriteAsync(await response.Content.ReadAsStringAsync()); + } await _next.Invoke(context); } diff --git a/src/Ocelot/Startup.cs b/src/Ocelot/Startup.cs index d9400980..18d5ba6b 100644 --- a/src/Ocelot/Startup.cs +++ b/src/Ocelot/Startup.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Ocelot.Library.Infrastructure.DownstreamRouteFinder; using Ocelot.Library.Middleware; namespace Ocelot @@ -36,6 +37,7 @@ namespace Ocelot // Add framework services. services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/test/Ocelot.AcceptanceTests/OcelotTests.cs b/test/Ocelot.AcceptanceTests/OcelotTests.cs index 6f270e2c..6eea27ab 100644 --- a/test/Ocelot.AcceptanceTests/OcelotTests.cs +++ b/test/Ocelot.AcceptanceTests/OcelotTests.cs @@ -90,7 +90,7 @@ namespace Ocelot.AcceptanceTests private void WhenIRequestTheUrlOnTheApiGateway(string url) { - _response = _client.GetAsync("/").Result; + _response = _client.GetAsync(url).Result; } private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinderTests.cs new file mode 100644 index 00000000..212e2029 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinderTests.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Options; +using Moq; +using Ocelot.Library.Infrastructure.Configuration; +using Ocelot.Library.Infrastructure.DownstreamRouteFinder; +using Ocelot.Library.Infrastructure.Responses; +using Ocelot.Library.Infrastructure.UrlMatcher; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests +{ + public class DownstreamRouteFinderTests + { + private readonly IDownstreamRouteFinder _downstreamRouteFinder; + private readonly Mock> _mockConfig; + private readonly Mock _mockMatcher; + private string _upstreamUrlPath; + private Response _result; + private Response _response; + private Configuration _configuration; + private UrlMatch _match; + + public DownstreamRouteFinderTests() + { + _mockConfig = new Mock>(); + _mockMatcher = new Mock(); + _downstreamRouteFinder = new DownstreamRouteFinder(_mockConfig.Object, _mockMatcher.Object); + } + + [Fact] + public void should_return_route() + { + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("somePath")) + .And(x => x.GivenTheConfigurationIs(new Configuration { + ReRoutes = new List + { + new ReRoute() + { + UpstreamTemplate = "somePath", + DownstreamTemplate = "somPath" + } + } + })) + .And(x => x.GivenTheUrlMatcherReturns(new UrlMatch(true, new List(), "somePath"))) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), "somePath"))) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_not_return_route() + { + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("somePath")) + .And(x => x.GivenTheConfigurationIs(new Configuration + { + ReRoutes = new List + { + new ReRoute() + { + UpstreamTemplate = "somePath", + DownstreamTemplate = "somPath" + } + } + })) + .And(x => x.GivenTheUrlMatcherReturns(new UrlMatch(false, new List(), null))) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenAnErrorResponseIsReturned()) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + + private void ThenAnErrorResponseIsReturned() + { + _result.IsError.ShouldBeTrue(); + } + + private void ThenTheUrlMatcherIsCalledCorrectly() + { + _mockMatcher + .Verify(x => x.Match(_upstreamUrlPath, _configuration.ReRoutes[0].UpstreamTemplate), Times.Once); + } + + private void GivenTheUrlMatcherReturns(UrlMatch match) + { + _match = match; + _mockMatcher + .Setup(x => x.Match(It.IsAny(), It.IsAny())) + .Returns(_match); + } + + private void GivenTheConfigurationIs(Configuration configuration) + { + _configuration = configuration; + _mockConfig + .Setup(x => x.Value) + .Returns(_configuration); + } + + private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) + { + _upstreamUrlPath = upstreamUrlPath; + } + + private void WhenICallTheFinder() + { + _result = _downstreamRouteFinder.FindDownstreamRoute(_upstreamUrlPath); + } + + private void ThenTheFollowingIsReturned(DownstreamRoute expected) + { + _result.Data.DownstreamUrlTemplate.ShouldBe(expected.DownstreamUrlTemplate); + for (int i = 0; i < _result.Data.TemplateVariableNameAndValues.Count; i++) + { + _result.Data.TemplateVariableNameAndValues[i].TemplateVariableName.ShouldBe( + expected.TemplateVariableNameAndValues[i].TemplateVariableName); + + _result.Data.TemplateVariableNameAndValues[i].TemplateVariableValue.ShouldBe( + expected.TemplateVariableNameAndValues[i].TemplateVariableValue); + } + + _result.IsError.ShouldBeFalse(); + } + } +} diff --git a/test/Ocelot.UnitTests/UpstreamUrlPathTemplateVariableReplacerTests.cs b/test/Ocelot.UnitTests/UpstreamUrlPathTemplateVariableReplacerTests.cs index 6b55c013..b280e04f 100644 --- a/test/Ocelot.UnitTests/UpstreamUrlPathTemplateVariableReplacerTests.cs +++ b/test/Ocelot.UnitTests/UpstreamUrlPathTemplateVariableReplacerTests.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Ocelot.Library.Infrastructure.DownstreamRouteFinder; using Ocelot.Library.Infrastructure.UrlMatcher; using Ocelot.Library.Infrastructure.UrlTemplateReplacer; using Shouldly; @@ -10,7 +11,7 @@ namespace Ocelot.UnitTests public class UpstreamUrlPathTemplateVariableReplacerTests { - private UrlMatch _urlMatch; + private DownstreamRoute _downstreamRoute; private string _result; private readonly IDownstreamUrlTemplateVariableReplacer _downstreamUrlPathReplacer; @@ -22,7 +23,7 @@ namespace Ocelot.UnitTests [Fact] public void can_replace_no_template_variables() { - this.Given(x => x.GivenThereIsAUrlMatch(new UrlMatch(true, new List(), ""))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), ""))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("")) .BDDfy(); @@ -31,7 +32,7 @@ namespace Ocelot.UnitTests [Fact] public void can_replace_no_template_variables_with_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new UrlMatch(true, new List(), "/"))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), "/"))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("/")) .BDDfy(); @@ -40,7 +41,7 @@ namespace Ocelot.UnitTests [Fact] public void can_replace_url_no_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new UrlMatch(true, new List(), "api"))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), "api"))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api")) .BDDfy(); @@ -49,7 +50,7 @@ namespace Ocelot.UnitTests [Fact] public void can_replace_url_one_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new UrlMatch(true, new List(), "api/"))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), "api/"))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api/")) .BDDfy(); @@ -58,7 +59,7 @@ namespace Ocelot.UnitTests [Fact] public void can_replace_url_multiple_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new UrlMatch(true, new List(), "api/product/products/"))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), "api/product/products/"))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api/product/products/")) .BDDfy(); @@ -72,7 +73,7 @@ namespace Ocelot.UnitTests new TemplateVariableNameAndValue("{productId}", "1") }; - this.Given(x => x.GivenThereIsAUrlMatch(new UrlMatch(true, templateVariables, "productservice/products/{productId}/"))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, "productservice/products/{productId}/"))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/")) .BDDfy(); @@ -86,7 +87,7 @@ namespace Ocelot.UnitTests new TemplateVariableNameAndValue("{productId}", "1") }; - this.Given(x => x.GivenThereIsAUrlMatch(new UrlMatch(true, templateVariables, "productservice/products/{productId}/variants"))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, "productservice/products/{productId}/variants"))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/variants")) .BDDfy(); @@ -101,7 +102,7 @@ namespace Ocelot.UnitTests new TemplateVariableNameAndValue("{variantId}", "12") }; - this.Given(x => x.GivenThereIsAUrlMatch(new UrlMatch(true, templateVariables, "productservice/products/{productId}/variants/{variantId}"))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, "productservice/products/{productId}/variants/{variantId}"))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/variants/12")) .BDDfy(); @@ -117,20 +118,20 @@ namespace Ocelot.UnitTests new TemplateVariableNameAndValue("{categoryId}", "34") }; - this.Given(x => x.GivenThereIsAUrlMatch(new UrlMatch(true, templateVariables, "productservice/category/{categoryId}/products/{productId}/variants/{variantId}"))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, "productservice/category/{categoryId}/products/{productId}/variants/{variantId}"))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/category/34/products/1/variants/12")) .BDDfy(); } - private void GivenThereIsAUrlMatch(UrlMatch urlMatch) + private void GivenThereIsAUrlMatch(DownstreamRoute downstreamRoute) { - _urlMatch = urlMatch; + _downstreamRoute = downstreamRoute; } private void WhenIReplaceTheTemplateVariables() { - _result = _downstreamUrlPathReplacer.ReplaceTemplateVariable(_urlMatch); + _result = _downstreamUrlPathReplacer.ReplaceTemplateVariable(_downstreamRoute); } private void ThenTheDownstreamUrlPathIsReturned(string expected) diff --git a/test/Ocelot.UnitTests/project.json b/test/Ocelot.UnitTests/project.json index f8c0cf9d..7b0acc6e 100644 --- a/test/Ocelot.UnitTests/project.json +++ b/test/Ocelot.UnitTests/project.json @@ -24,7 +24,8 @@ "dotnet-test-xunit": "2.2.0-preview2-build1029", "Shouldly": "2.8.0", "TestStack.BDDfy": "4.3.1", - "YamlDotNet": "3.9.0" + "YamlDotNet": "3.9.0", + "Moq": "4.6.38-alpha" }, "frameworks": {