diff --git a/run-acceptance-tests.bat b/run-acceptance-tests.bat new file mode 100755 index 00000000..ba8a3489 --- /dev/null +++ b/run-acceptance-tests.bat @@ -0,0 +1,8 @@ +echo Running Ocelot.AcceptanceTests +cd test/Ocelot.AcceptanceTests/ +dotnet restore +dotnet test +cd ../../ + +echo Restoring Ocelot.ManualTest +dotnet restore test/Ocelot.ManualTest/ \ No newline at end of file diff --git a/run-tests.bat b/run-tests.bat index ae4856fc..39532229 100755 --- a/run-tests.bat +++ b/run-tests.bat @@ -1,17 +1,2 @@ -echo ------------------------- - -echo Restoring Ocelot -dotnet restore src/Ocelot - -echo Restoring Ocelot.ManualTest -dotnet restore test/Ocelot.ManualTest/ - -echo Running Ocelot.UnitTests -dotnet restore test/Ocelot.UnitTests/ -dotnet test test/Ocelot.UnitTests/ - -echo Running Ocelot.AcceptanceTests -cd test/Ocelot.AcceptanceTests/ -dotnet restore -dotnet test -cd ../../ \ No newline at end of file +./run-unit-tests.bat +./run-acceptance-tests.bat \ No newline at end of file diff --git a/run-unit-tests.bat b/run-unit-tests.bat new file mode 100755 index 00000000..9ad6a4f2 --- /dev/null +++ b/run-unit-tests.bat @@ -0,0 +1,8 @@ +echo ------------------------- + +echo Restoring Ocelot +dotnet restore src/Ocelot + +echo Running Ocelot.UnitTests +dotnet restore test/Ocelot.UnitTests/ +dotnet test test/Ocelot.UnitTests/ diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index f77ae66b..3f5ef40b 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Ocelot.Configuration.Builder { @@ -23,12 +24,54 @@ namespace Ocelot.Configuration.Builder private string _requestIdHeaderKey; private bool _isCached; private CacheOptions _fileCacheOptions; + private bool _useServiceDiscovery; + private string _serviceName; + private string _serviceDiscoveryProvider; + private string _serviceDiscoveryAddress; + private string _downstreamScheme; + private string _downstreamHost; public ReRouteBuilder() { _additionalScopes = new List(); } + public ReRouteBuilder WithDownstreamScheme(string downstreamScheme) + { + _downstreamScheme = downstreamScheme; + return this; + } + + public ReRouteBuilder WithDownstreamHost(string downstreamHost) + { + _downstreamHost = downstreamHost; + return this; + } + + public ReRouteBuilder WithServiceDiscoveryAddress(string serviceDiscoveryAddress) + { + _serviceDiscoveryAddress = serviceDiscoveryAddress; + return this; + } + + public ReRouteBuilder WithServiceDiscoveryProvider(string serviceDiscoveryProvider) + { + _serviceDiscoveryProvider = serviceDiscoveryProvider; + return this; + } + + public ReRouteBuilder WithServiceName(string serviceName) + { + _serviceName = serviceName; + return this; + } + + public ReRouteBuilder WithUseServiceDiscovery(bool useServiceDiscovery) + { + _useServiceDiscovery = useServiceDiscovery; + return this; + } + public ReRouteBuilder WithDownstreamTemplate(string input) { _downstreamTemplate = input; @@ -143,10 +186,13 @@ namespace Ocelot.Configuration.Builder public ReRoute Build() { + Func downstreamHostFunc = () => { return _downstreamHost; }; + return new ReRoute(_downstreamTemplate, _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, _requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement, - _isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions); + _isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _serviceName, + _useServiceDiscovery, _serviceDiscoveryAddress, _serviceDiscoveryProvider, downstreamHostFunc, _downstreamScheme); } } } diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index f74f880b..c71aba23 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -91,6 +91,13 @@ namespace Ocelot.Configuration.Creator ? globalConfiguration.RequestIdKey : reRoute.RequestIdKey; + var useServiceDiscovery = !string.IsNullOrEmpty(reRoute.ServiceName) + && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Address) + && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider); + + + Func downstreamHostFunc = () => { return reRoute.DownstreamHost; }; + if (isAuthenticated) { var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider, @@ -106,14 +113,18 @@ namespace Ocelot.Configuration.Creator reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, authOptionsForRoute, claimsToHeaders, claimsToClaims, reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries, - requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds)); + requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds), + reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider, + globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostFunc, reRoute.DownstreamScheme); } 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)); + requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds), + reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider, + globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostFunc, reRoute.DownstreamScheme); } private string BuildUpstreamTemplate(FileReRoute reRoute) diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 07c17188..f414bc83 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -2,6 +2,11 @@ { public class FileGlobalConfiguration { + public FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider(); + } public string RequestIdKey { get; set; } + public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;} } } diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index 3773dd9d..3afa03ce 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -25,5 +25,8 @@ namespace Ocelot.Configuration.File public string RequestIdKey { get; set; } public FileCacheOptions FileCacheOptions { get; set; } public bool ReRouteIsCaseSensitive { get; set; } + public string ServiceName { get; set; } + public string DownstreamScheme {get;set;} + public string DownstreamHost {get;set;} } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs new file mode 100644 index 00000000..47efc6df --- /dev/null +++ b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs @@ -0,0 +1,8 @@ +namespace Ocelot.Configuration.File +{ + public class FileServiceDiscoveryProvider + { + public string Provider {get;set;} + public string Address {get;set;} + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index 433250b3..8778e5f7 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Ocelot.Configuration { @@ -7,7 +8,8 @@ namespace Ocelot.Configuration public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, bool isAuthenticated, AuthenticationOptions authenticationOptions, List configurationHeaderExtractorProperties, List claimsToClaims, Dictionary routeClaimsRequirement, bool isAuthorised, List claimsToQueries, - string requestIdKey, bool isCached, CacheOptions fileCacheOptions) + string requestIdKey, bool isCached, CacheOptions fileCacheOptions, string serviceName, bool useServiceDiscovery, + string serviceDiscoveryProvider, string serviceDiscoveryAddress, Func downstreamHost, string downstreamScheme) { DownstreamTemplate = downstreamTemplate; UpstreamTemplate = upstreamTemplate; @@ -26,6 +28,12 @@ namespace Ocelot.Configuration ?? new List(); ClaimsToHeaders = configurationHeaderExtractorProperties ?? new List(); + ServiceName = serviceName; + UseServiceDiscovery = useServiceDiscovery; + ServiceDiscoveryProvider = serviceDiscoveryProvider; + ServiceDiscoveryAddress = serviceDiscoveryAddress; + DownstreamHost = downstreamHost; + DownstreamScheme = downstreamScheme; } public string DownstreamTemplate { get; private set; } @@ -42,5 +50,11 @@ namespace Ocelot.Configuration public string RequestIdKey { get; private set; } public bool IsCached { get; private set; } public CacheOptions FileCacheOptions { get; private set; } + public string ServiceName { get; private set;} + public bool UseServiceDiscovery { get; private set;} + public string ServiceDiscoveryProvider { get; private set;} + public string ServiceDiscoveryAddress { get; private set;} + public Func DownstreamHost {get;private set;} + public string DownstreamScheme {get;private set;} } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Validator/DownstreamTemplateContainsHostError.cs b/src/Ocelot/Configuration/Validator/DownstreamTemplateContainsHostError.cs new file mode 100644 index 00000000..8d9dba92 --- /dev/null +++ b/src/Ocelot/Configuration/Validator/DownstreamTemplateContainsHostError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class DownstreamTemplateContainsHostError : Error + { + public DownstreamTemplateContainsHostError(string message) + : base(message, OcelotErrorCode.DownstreamTemplateContainsHostError) + { + } + } +} diff --git a/src/Ocelot/Configuration/Validator/DownstreamTemplateContainsSchemeError.cs b/src/Ocelot/Configuration/Validator/DownstreamTemplateContainsSchemeError.cs new file mode 100644 index 00000000..1901fc88 --- /dev/null +++ b/src/Ocelot/Configuration/Validator/DownstreamTemplateContainsSchemeError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class DownstreamTemplateContainsSchemeError : Error + { + public DownstreamTemplateContainsSchemeError(string message) + : base(message, OcelotErrorCode.DownstreamTemplateContainsSchemeError) + { + } + } +} diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs index fb55a10b..3a675d41 100644 --- a/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs +++ b/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs @@ -26,6 +26,13 @@ namespace Ocelot.Configuration.Validator return new OkResponse(result); } + result = CheckForReRoutesContainingDownstreamScheme(configuration); + + if (result.IsError) + { + return new OkResponse(result); + } + return new OkResponse(result); } @@ -63,6 +70,27 @@ namespace Ocelot.Configuration.Validator return Enum.TryParse(provider, true, out supportedProvider); } + private ConfigurationValidationResult CheckForReRoutesContainingDownstreamScheme(FileConfiguration configuration) + { + var errors = new List(); + + foreach(var reRoute in configuration.ReRoutes) + { + if(reRoute.DownstreamTemplate.Contains("https://") + || reRoute.DownstreamTemplate.Contains("http://")) + { + errors.Add(new DownstreamTemplateContainsSchemeError($"{reRoute.DownstreamTemplate} contains scheme")); + } + } + + if(errors.Any()) + { + return new ConfigurationValidationResult(false, errors); + } + + return new ConfigurationValidationResult(true, errors); + } + private ConfigurationValidationResult CheckForDupliateReRoutes(FileConfiguration configuration) { var hasDupes = configuration.ReRoutes diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index a4864d1a..f0a336f0 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -17,6 +17,8 @@ InstructionNotForClaimsError, UnauthorizedError, ClaimValueNotAuthorisedError, - UserDoesNotHaveClaimError + UserDoesNotHaveClaimError, + DownstreamTemplateContainsSchemeError, + DownstreamTemplateContainsHostError } } diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs index 7f6f4005..174ea8bc 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs @@ -19,6 +19,44 @@ namespace Ocelot.UnitTests.Configuration _configurationValidator = new FileConfigurationValidator(); } + [Fact] + public void configuration_is_invalid_if_scheme_in_downstream_template() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration() + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "http://www.bbc.co.uk/api/products/{productId}", + UpstreamTemplate = "http://asdf.com" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_if_host_in_downstream_template() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration() + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamTemplate = "www.bbc.co.uk/api/products/{productId}", + UpstreamTemplate = "http://asdf.com" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .BDDfy(); + } + [Fact] public void configuration_is_valid_with_one_reroute() { @@ -28,7 +66,7 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - DownstreamTemplate = "http://www.bbc.co.uk", + DownstreamTemplate = "/api/products/", UpstreamTemplate = "http://asdf.com" } } @@ -47,7 +85,7 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - DownstreamTemplate = "http://www.bbc.co.uk", + DownstreamTemplate = "/api/products/", UpstreamTemplate = "http://asdf.com", AuthenticationOptions = new FileAuthenticationOptions { @@ -70,7 +108,7 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - DownstreamTemplate = "http://www.bbc.co.uk", + DownstreamTemplate = "/api/products/", UpstreamTemplate = "http://asdf.com", AuthenticationOptions = new FileAuthenticationOptions { @@ -94,7 +132,7 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - DownstreamTemplate = "http://www.bbc.co.uk", + DownstreamTemplate = "/api/products/", UpstreamTemplate = "http://asdf.com" }, new FileReRoute diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 4a4a5345..4207a333 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -35,6 +35,143 @@ namespace Ocelot.UnitTests.Configuration _fileConfig.Object, _validator.Object, _configParser.Object, _logger.Object); } + [Fact] + public void should_use_downstream_host() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHost = "127.0.0.1", + UpstreamTemplate = "/api/products/{productId}", + DownstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamHost("127.0.0.1") + .WithDownstreamTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .Build() + })) + .BDDfy(); + } + + public void should_use_downstream_scheme() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamScheme = "https", + UpstreamTemplate = "/api/products/{productId}", + DownstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamScheme("https") + .WithDownstreamTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_use_service_discovery_for_downstream_service_host() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = false, + ServiceName = "ProductService" + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Provider = "consul", + Address = "127.0.0.1" + } + } + })) + .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/.*/$") + .WithServiceName("ProductService") + .WithUseServiceDiscovery(true) + .WithServiceDiscoveryProvider("consul") + .WithServiceDiscoveryAddress("127.0.01") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_not_use_service_discovery_for_downstream_host_url_when_no_service_name() + { + 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/.*/$") + .WithUseServiceDiscovery(false) + .WithServiceDiscoveryProvider(null) + .WithServiceDiscoveryAddress(null) + .Build() + })) + .BDDfy(); + } + [Fact] public void should_use_reroute_case_sensitivity_value() { diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs index 1bdb3279..56fb6487 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; +using System.Collections.Generic; using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Creator;