diff --git a/.gitignore b/.gitignore index 61b1c211..f96b1d02 100644 --- a/.gitignore +++ b/.gitignore @@ -235,3 +235,10 @@ _Pvt_Extensions # FAKE - F# Make .fake/ +tools/ + +# MacOS +.DS_Store + +# Ocelot acceptance test config +test/Ocelot.AcceptanceTests/configuration.json \ No newline at end of file diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 00000000..05e9ac41 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,4 @@ +mode: ContinuousDelivery +branches: {} +ignore: + sha: [] diff --git a/Ocelot.nuspec b/Ocelot.nuspec deleted file mode 100644 index f9875da0..00000000 --- a/Ocelot.nuspec +++ /dev/null @@ -1,44 +0,0 @@ - - - - Ocelot - 1.0.0 - Tom Pallister - Tom Pallister - https://github.com/TomPallister/Ocelot/blob/develop/LICENSE.md - https://github.com/TomPallister/Ocelot - false - Ocelot Api Gateway - Latest Ocelot - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Ocelot.sln b/Ocelot.sln index 5ebd0660..9a34aed2 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -8,17 +8,24 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore - appveyor.yml = appveyor.yml - build-and-run-tests.bat = build-and-run-tests.bat - build.bat = build.bat + build-and-release-unstable.ps1 = build-and-release-unstable.ps1 + build-and-run-tests.ps1 = build-and-run-tests.ps1 + build.cake = build.cake + build.ps1 = build.ps1 + build.readme.md = build.readme.md configuration-explanation.txt = configuration-explanation.txt + configuration.yaml = configuration.yaml + GitVersion.yml = GitVersion.yml global.json = global.json LICENSE.md = LICENSE.md - Ocelot.nuspec = Ocelot.nuspec - push-to-nuget.bat = push-to-nuget.bat + ocelot.postman_collection.json = ocelot.postman_collection.json README.md = README.md - run-benchmarks.bat = run-benchmarks.bat - run-tests.bat = run-tests.bat + release.ps1 = release.ps1 + ReleaseNotes.md = ReleaseNotes.md + run-acceptance-tests.ps1 = run-acceptance-tests.ps1 + run-benchmarks.ps1 = run-benchmarks.ps1 + run-unit-tests.ps1 = run-unit-tests.ps1 + version.ps1 = version.ps1 EndProjectSection EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot", "src\Ocelot\Ocelot.xproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" @@ -33,6 +40,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.ManualTest", "test\O EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.Benchmarks", "test\Ocelot.Benchmarks\Ocelot.Benchmarks.xproj", "{106B49E6-95F6-4A7B-B81C-96BFA74AF035}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.xproj", "{D4575572-99CA-4530-8737-C296EDA326F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +68,10 @@ Global {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.Build.0 = Debug|Any CPU {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.ActiveCfg = Release|Any CPU {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.Build.0 = Release|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -69,5 +82,6 @@ Global {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {5B401523-36DA-4491-B73A-7590A26E420B} {02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B} {106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B} + {D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index dd5f1043..642f843f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Ocelot -[![Build status](https://ci.appveyor.com/api/projects/status/roahbe4nl526ysya?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot) +[![Build status](https://ci.appveyor.com/api/projects/status/r6sv51qx36sis1je?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb) [![Join the chat at https://gitter.im/Ocelotey/Lobby](https://badges.gitter.im/Ocelotey/Lobby.svg)](https://gitter.im/Ocelotey/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) @@ -30,9 +30,6 @@ and retrived as the requests goes back up the Ocelot pipeline. There is a piece that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features. -This is not ready for production yet as uses a lot of rc and beta .net core packages. -Hopefully by the start of 2017 it will be in use. - ## Contributing Pull requests, issues and commentary welcome! No special process just create a request and get in @@ -41,13 +38,13 @@ touch either via gitter or create an issue. ## How to install Ocelot is designed to work with ASP.NET core only and is currently -built to netcoreapp1.4 [this](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) documentation may prove helpful when working out if Ocelot would be suitable for you. +built to netcoreapp1.1 [this](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) documentation may prove helpful when working out if Ocelot would be suitable for you. Install Ocelot and it's dependecies using nuget. At the moment all we have is the pre version. Once we have something working in a half decent way we will drop a version. -`Install-Package Ocelot -Pre` +`Install-Package Ocelot` All versions can be found [here](https://www.nuget.org/packages/Ocelot/) @@ -61,11 +58,12 @@ The ReRoutes are the objects that tell Ocelot how to treat an upstream request. 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": {} - } - +```json +{ + "ReRoutes": [], + "GlobalConfiguration": {} +} +``` More information on how to use these options is below.. ## Startup @@ -73,46 +71,75 @@ More information on how to use these options is below.. An example startup using a json file for configuration can be seen below. Currently this is the only way to get configuration into Ocelot. - public class Startup +```csharp +public class Startup +{ + public Startup(IHostingEnvironment env) { - public Startup(IHostingEnvironment env) + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddJsonFile("configuration.json") + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + Action settings = (x) => { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddJsonFile("configuration.json") - .AddEnvironmentVariables(); - - Configuration = builder.Build(); - } - - public IConfigurationRoot Configuration { get; } - - public void ConfigureServices(IServiceCollection services) - { - Action settings = (x) => + x.WithMicrosoftLogging(log => { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithDictionaryHandle(); - }; + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; services.AddOcelotOutputCaching(settings); - services.AddOcelotFileConfiguration(Configuration); - services.AddOcelot(); + services.AddOcelot(Configuration); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); - app.UseOcelot(); + await app.UseOcelot(); } } +} +``` +Then in your Program.cs you will want to have the following. This can be changed if you +don't wan't to use the default url e.g. UseUrls(someUrls) and should work as long as you keep the WebHostBuilder registration. + +```csharp + public class Program + { + public static void Main(string[] args) + { + IWebHostBuilder builder = new WebHostBuilder(); + + builder.ConfigureServices(s => { + s.AddSingleton(builder); + }); + + builder.UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup(); + + var host = builder.Build(); + + host.Run(); + } + } +``` + +Sadly we need to inject the IWebHostBuilder interface to get the applications scheme, url and port later. I cannot +find a better way of doing this at the moment without setting this in a static or some kind of config. This is pretty much all you need to get going.......more to come! @@ -122,59 +149,151 @@ Ocelot's primary functionality is to take incomeing http requests and forward th to a downstream service. At the moment in the form of another http request (in the future this could be any transport mechanism.). -Ocelot always adds a trailing slash to an UpstreamTemplate. +Ocelot always adds a trailing slash to an UpstreamPathTemplate. Ocelot's describes the routing of one request to another as a ReRoute. In order to get anything working in Ocelot you need to set up a ReRoute in the configuration. - { - "ReRoutes": [ - ] - } +```json +{ + "ReRoutes": [ + ] +} +``` In order to set up a ReRoute you need to add one to the json array called ReRoutes like the following. - { - "DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts/{postId}", - "UpstreamTemplate": "/posts/{postId}", - "UpstreamHttpMethod": "Put" - } +```json +{ + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "DownstreamPort": 80, + "DownstreamHost" "localhost" + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Put" +} +``` -The DownstreamTemplate is the URL that this request will be forwarded to. -The UpstreamTemplate is the URL that Ocelot will use to identity which -DownstreamTemplate to use for a given request. Finally the UpstreamHttpMethod is used so +The DownstreamPathTemplate,Scheme, Port and Host make the URL that this request will be forwarded to. +The UpstreamPathTemplate is the URL that Ocelot will use to identity which +DownstreamPathTemplate to use for a given request. Finally the UpstreamHttpMethod is used so Ocelot can distinguish between requests to the same URL and is obviously needed to work :) In Ocelot you can add placeholders for variables to your Templates in the form of {something}. -The placeholder needs to be in both the DownstreamTemplate and UpstreamTemplate. If it is +The placeholder needs to be in both the DownstreamPathTemplate and UpstreamPathTemplate. If it is Ocelot will attempt to replace the placeholder with the correct variable value from the Upstream URL when the request comes in. 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 +```json +"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! +## Administration + +Ocelot supports changing configuration during runtime via an authenticated HTTP API. The API is authenticated +using bearer tokens that you request from iteself. This is provided by the amazing [IdentityServer](https://github.com/IdentityServer/IdentityServer4) +project that I have been using for a few years now. Check them out. + +In order to enable the administration section you need to do a few things. First of all add this to your +initial configuration.json. The value can be anything you want and it is obviously reccomended don't use +a url you would like to route through with Ocelot as this will not work. The administration uses the +MapWhen functionality of asp.net core and all requests to root/administration will be sent there not +to the Ocelot middleware. + +```json +"GlobalConfiguration": { + "AdministrationPath": "/administration" +} +``` +This will get the admin area set up but not the authentication. Please note that this is a very basic approach to +this problem and if needed we can obviously improve on this! + +You need to set 3 environmental variables. + + OCELOT_USERNAME + OCELOT_HASH + OCELOT_SALT + +These need to be the admin username you want to use with Ocelot and the hash and salt of the password you want to +use given hashing algorythm. When requesting bearer tokens for use with the administration api you will need to +supply username and password. + +In order to create a hash and salt of your password please check out HashCreationTests.should_create_hash_and_salt() +this technique is based on [this](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing) +using SHA256 rather than SHA1. + +Now if you went with the configuration options above and want to access the API you can use the postman scripts +called ocelot.postman_collection.json in the solution to change the Ocelot configuration. Obviously these +will need to be changed if you are running Ocelot on a different url to http://localhost:5000. + +The scripts show you how to request a bearer token from ocelot and then use it to GET the existing configuration and POST +a configuration. + +## Service Discovery + +Ocelot allows you to specify a service discovery provider and will use this to find the host and port +for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the +GlobalConfiguration section which means the same service discovery provider will be used for all ReRoutes +you specify a ServiceName for at ReRoute level. + +In the future we can add a feature that allows ReRoute specfic configuration. + +At the moment the only supported service discovery provider is Consul. The following is required in the +GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default +will be used. + +```json +"ServiceDiscoveryProvider": { + "Provider":"Consul", + "Host":"localhost", + "Port":8500 +} +``` + +In order to tell Ocelot a ReRoute is to use the service discovery provider for its host and port you must add the +ServiceName and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin +and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests. + +```json +{ + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Put", + "ServiceName": "product", + "LoadBalancer": "LeastConnection" +} +``` + +When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balancer +requests across any available services. + + ## Authentication Ocelot currently supports the use of bearer tokens with Identity Server (more providers to come if required). In order to identity a ReRoute as authenticated it needs the following configuration added. - "AuthenticationOptions": { - "Provider": "IdentityServer", - "ProviderRootUrl": "http://localhost:52888", - "ScopeName": "api", - "AdditionalScopes": [ - "openid", - "offline_access" - ], - "ScopeSecret": "secret" - } +```json +"AuthenticationOptions": { + "Provider": "IdentityServer", + "ProviderRootUrl": "http://localhost:52888", + "ScopeName": "api", + "AdditionalScopes": [ + "openid", + "offline_access" + ], + "ScopeSecret": "secret" +} +``` In this example the Provider is specified as IdentityServer. This string is important because it is used to identity the authentication provider (as previously mentioned in @@ -193,9 +312,11 @@ is 401 unauthorised. Ocelot supports claims based authorisation which is run post authentication. This means if you have a route you want to authorise you can add the following to you ReRoute configuration. - "RouteClaimsRequirement": { - "UserType": "registered" - }, +```json +"RouteClaimsRequirement": { + "UserType": "registered" +}, +``` In this example when the authorisation middleware is called Ocelot will check to see if the user has the claim type UserType and if the value of that claim is registered. @@ -234,10 +355,12 @@ and add whatever was at the index requested to the transform. Below is an example configuration that will transforms claims to claims - "AddClaimsToRequest": { - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, +```json + "AddClaimsToRequest": { + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" +}, +``` This shows a transforms where Ocelot looks at the users sub claim and transforms it into UserType and UserId claims. Assuming the sub looks like this "usertypevalue|useridvalue". @@ -246,9 +369,11 @@ UserType and UserId claims. Assuming the sub looks like this "usertypevalue|user Below is an example configuration that will transforms claims to headers - "AddHeadersToRequest": { - "CustomerId": "Claims[sub] > value[1] > |" - }, +```json +"AddHeadersToRequest": { + "CustomerId": "Claims[sub] > value[1] > |" +}, +``` This shows a transform where Ocelot looks at the users sub claim and trasnforms it into a CustomerId header. Assuming the sub looks like this "usertypevalue|useridvalue". @@ -257,26 +382,36 @@ CustomerId header. Assuming the sub looks like this "usertypevalue|useridvalue". Below is an example configuration that will transforms claims to query string parameters - "AddQueriesToRequest": { - "LocationId": "Claims[LocationId] > value", - }, +```json +"AddQueriesToRequest": { + "LocationId": "Claims[LocationId] > value", +}, +``` This shows a transform where Ocelot looks at the users LocationId claim and add its as a query string parameter to be forwarded onto the downstream service. -## Logging +## Quality of Service -Ocelot uses the standard logging interfaces ILoggerFactory / ILogger at the moment. -This is encapsulated in IOcelotLogger / IOcelotLoggerFactory with an implementation -for the standard asp.net core logging stuff at the moment. +Ocelot supports one QoS capability at the current time. You can set on a per ReRoute basis if you +want to use a circuit breaker when making requests to a downstream service. This uses the an awesome +.NET library called Polly check them out [here](https://github.com/App-vNext/Polly). -There are a bunch of debugging logs in the ocelot middlewares however I think the -system probably needs more logging in the code it calls into. Other than the debugging -there is a global error handler that should catch any errors thrown and log them as errors. +Add the following section to a ReRoute configuration. -The reason for not just using bog standard framework logging is that I could not -work out how to override the request id that get's logged when setting IncludeScopes -to true for logging settings. Nicely onto the next feature. +```json +"QoSOptions": { + "ExceptionsAllowedBeforeBreaking":3, + "DurationOfBreak":5, + "TimeoutValue":5000 +} +``` + +You must set a number greater than 0 against ExceptionsAllowedBeforeBreaking for this rule to be +implemented. Duration of break is how long the circuit breaker will stay open for after it is tripped. +TimeoutValue means ff a request takes more than 5 seconds it will automatically be timed out. + +If you do not add a QoS section QoS will not be used. ## RequestId / CorrelationId @@ -291,14 +426,18 @@ have an OcelotRequestId. In order to use the requestid feature in your ReRoute configuration add this setting - "RequestIdKey": "OcRequestId" +```json +"RequestIdKey": "OcRequestId" +``` 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", +```json +"RequestIdKey": "OcRequestId", +``` It behaves in exactly the same way as the ReRoute level RequestIdKey settings. @@ -317,7 +456,9 @@ and setting a TTL in seconds to expire the cache. More to come! In orde to use caching on a route in your ReRoute configuration add this setting. - "FileCacheOptions": { "TtlSeconds": 15 } +```json +"FileCacheOptions": { "TtlSeconds": 15 } +``` In this example ttl seconds is set to 15 which means the cache will expire after 15 seconds. @@ -329,15 +470,17 @@ pipeline and you are using any of the following. Remove them and try again! When setting up Ocelot in your Startup.cs you can provide some additonal middleware and override middleware. This is done as follos. - var configuration = new OcelotMiddlewareConfiguration - { - PreErrorResponderMiddleware = async (ctx, next) => - { - await next.Invoke(); - } - }; +```csharp +var configuration = new OcelotMiddlewareConfiguration +{ + PreErrorResponderMiddleware = async (ctx, next) => + { + await next.Invoke(); + } +}; - app.UseOcelot(configuration); +app.UseOcelot(configuration); +``` In the example above the provided function will run before the first piece of Ocelot middleware. This allows a user to supply any behaviours they want before and after the Ocelot pipeline has run. @@ -363,6 +506,20 @@ http request before it is passed to Ocelots request creator. Obviously you can just add middleware as normal before the call to app.UseOcelot() It cannot be added after as Ocelot does not call the next middleware. +## Logging + +Ocelot uses the standard logging interfaces ILoggerFactory / ILogger at the moment. +This is encapsulated in IOcelotLogger / IOcelotLoggerFactory with an implementation +for the standard asp.net core logging stuff at the moment. + +There are a bunch of debugging logs in the ocelot middlewares however I think the +system probably needs more logging in the code it calls into. Other than the debugging +there is a global error handler that should catch any errors thrown and log them as errors. + +The reason for not just using bog standard framework logging is that I could not +work out how to override the request id that get's logged when setting IncludeScopes +to true for logging settings. Nicely onto the next feature. + ## Not supported Ocelot does not support... @@ -381,8 +538,11 @@ 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. +[![](https://codescene.io/projects/697/status.svg) Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results) + ## Coming up You can see what we are working on [here](https://github.com/TomPallister/Ocelot/projects/1) + diff --git a/README.md.orig b/README.md.orig new file mode 100644 index 00000000..6c3ad795 --- /dev/null +++ b/README.md.orig @@ -0,0 +1,564 @@ +# Ocelot + +[![Build status](https://ci.appveyor.com/api/projects/status/r6sv51qx36sis1je?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb) + +[![Join the chat at https://gitter.im/Ocelotey/Lobby](https://badges.gitter.im/Ocelotey/Lobby.svg)](https://gitter.im/Ocelotey/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +[![](https://codescene.io/projects/697/status.svg) Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results) + +Attempt at a .NET Api Gateway + +This project is aimed at people using .NET running +a micro services / service orientated architecture +that need a unified point of entry into their system. + +In particular I want easy integration with +IdentityServer reference and bearer tokens. + +We have been unable to find this in my current workplace +without having to write our own Javascript middlewares +to handle the IdentityServer reference tokens. We would +rather use the IdentityServer code that already exists +to do this. + +Ocelot is a bunch of middlewares in a specific order. + +Ocelot manipulates the HttpRequest object into a state specified by its configuration until +it reaches a request builder middleware where it creates a HttpRequestMessage object which is +used to make a request to a downstream service. The middleware that makes the request is +the last thing in the Ocelot pipeline. It does not call the next middleware. +The response from the downstream service is stored in a per request scoped repository +and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware +that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. +That is basically it with a bunch of other features. + +## Contributing + +Pull requests, issues and commentary welcome! No special process just create a request and get in +touch either via gitter or create an issue. + +## How to install + +Ocelot is designed to work with ASP.NET core only and is currently +built to netcoreapp1.1 [this](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) documentation may prove helpful when working out if Ocelot would be suitable for you. + +Install Ocelot and it's dependecies using nuget. At the moment +all we have is the pre version. Once we have something working in +a half decent way we will drop a version. + +`Install-Package Ocelot` + +All versions can be found [here](https://www.nuget.org/packages/Ocelot/) + +## Configuration + +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. + +```json +{ + "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. +Currently this is the only way to get configuration into Ocelot. + +<<<<<<< HEAD + public class Startup +======= +```csharp +public class Startup +{ + public Startup(IHostingEnvironment env) +>>>>>>> develop + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddJsonFile("configuration.json") + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + Action settings = (x) => + { + x.WithMicrosoftLogging(log => + { +<<<<<<< HEAD + x.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; + + services.AddOcelotOutputCaching(settings); + services.AddOcelot(Configuration); + } + + public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + + await app.UseOcelot(); + } + } + +Then in your Program.cs you will want to have the following. This can be changed if you +don't wan't to use the default url e.g. UseUrls(someUrls) and should work as long as you keep the WebHostBuilder registration. + + IWebHostBuilder builder = new WebHostBuilder(); + + builder.ConfigureServices(s => { + s.AddSingleton(builder); + }); + + builder.UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup(); + + var host = builder.Build(); + + host.Run(); + +Sadly we need to inject the IWebHostBuilder interface to get the applications scheme, url and port later. I cannot +find a better way of doing this at the moment without setting this in a static or some kind of config. +======= + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; + + services.AddOcelotOutputCaching(settings); + services.AddOcelotFileConfiguration(Configuration); + services.AddOcelot(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + + app.UseOcelot(); + } +} +``` +>>>>>>> develop + +This is pretty much all you need to get going.......more to come! + +## Routing + +Ocelot's primary functionality is to take incomeing http requests and forward them on +to a downstream service. At the moment in the form of another http request (in the future +this could be any transport mechanism.). + +Ocelot always adds a trailing slash to an UpstreamPathTemplate. + +Ocelot's describes the routing of one request to another as a ReRoute. In order to get +anything working in Ocelot you need to set up a ReRoute in the configuration. + +```json +{ + "ReRoutes": [ + ] +} +``` + +In order to set up a ReRoute you need to add one to the json array called ReRoutes like +the following. + +```json +{ + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "DownstreamPort": 80, + "DownstreamHost" "localhost" + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Put" +} +``` + +The DownstreamPathTemplate,Scheme, Port and Host make the URL that this request will be forwarded to. +The UpstreamPathTemplate is the URL that Ocelot will use to identity which +DownstreamPathTemplate to use for a given request. Finally the UpstreamHttpMethod is used so +Ocelot can distinguish between requests to the same URL and is obviously needed to work :) +In Ocelot you can add placeholders for variables to your Templates in the form of {something}. +The placeholder needs to be in both the DownstreamPathTemplate and UpstreamPathTemplate. If it is +Ocelot will attempt to replace the placeholder with the correct variable value from the +Upstream URL when the request comes in. + +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. + +```json +"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! + +## Administration + +Ocelot supports changing configuration during runtime via an authenticated HTTP API. The API is authenticated +using bearer tokens that you request from iteself. This is provided by the amazing [IdentityServer](https://github.com/IdentityServer/IdentityServer4) +project that I have been using for a few years now. Check them out. + +In order to enable the administration section you need to do a few things. First of all add this to your +initial configuration.json. The value can be anything you want and it is obviously reccomended don't use +a url you would like to route through with Ocelot as this will not work. The administration uses the +MapWhen functionality of asp.net core and all requests to root/administration will be sent there not +to the Ocelot middleware. + + "GlobalConfiguration": { + "AdministrationPath": "/administration" + } + +This will get the admin area set up but not the authentication. Please note that this is a very basic approach to +this problem and if needed we can obviously improve on this! + +You need to set 3 environmental variables. + + OCELOT_USERNAME + OCELOT_HASH + OCELOT_SALT + +These need to be the admin username you want to use with Ocelot and the hash and salt of the password you want to +use given hashing algorythm. When requesting bearer tokens for use with the administration api you will need to +supply username and password. + +In order to create a hash and salt of your password please check out HashCreationTests.should_create_hash_and_salt() +this technique is based on [this](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing) +using SHA256 rather than SHA1. + +Now if you went with the configuration options above and want to access the API you can use the postman scripts +called ocelot.postman_collection.json in the solution to change the Ocelot configuration. Obviously these +will need to be changed if you are running Ocelot on a different url to http://localhost:5000. + +The scripts show you how to request a bearer token from ocelot and then use it to GET the existing configuration and POST +a configuration. + +## Service Discovery + +Ocelot allows you to specify a service discovery provider and will use this to find the host and port +for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the +GlobalConfiguration section which means the same service discovery provider will be used for all ReRoutes +you specify a ServiceName for at ReRoute level. + +In the future we can add a feature that allows ReRoute specfic configuration. + +At the moment the only supported service discovery provider is Consul. The following is required in the +GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default +will be used. + +```json +"ServiceDiscoveryProvider": { + "Provider":"Consul", + "Host":"localhost", + "Port":8500 +} +``` + +In order to tell Ocelot a ReRoute is to use the service discovery provider for its host and port you must add the +ServiceName and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin +and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests. + +```json +{ + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Put", + "ServiceName": "product", + "LoadBalancer": "LeastConnection" +} +``` + +When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balancer +requests across any available services. + + +## Authentication + +Ocelot currently supports the use of bearer tokens with Identity Server (more providers to +come if required). In order to identity a ReRoute as authenticated it needs the following +configuration added. + +```json +"AuthenticationOptions": { + "Provider": "IdentityServer", + "ProviderRootUrl": "http://localhost:52888", + "ScopeName": "api", + "AdditionalScopes": [ + "openid", + "offline_access" + ], + "ScopeSecret": "secret" +} +``` + +In this example the Provider is specified as IdentityServer. This string is important +because it is used to identity the authentication provider (as previously mentioned in +the future there might be more providers). Identity server requires that the client +talk to it so we need to provide the root url of the IdentityServer as ProviderRootUrl. +IdentityServer requires at least one scope and you can also provider additional scopes. +Finally if you are using IdentityServer reference tokens you need to provide the scope +secret. + +Ocelot will use this configuration to build an authentication handler and if +authentication is succefull the next middleware will be called else the response +is 401 unauthorised. + +## Authorisation + +Ocelot supports claims based authorisation which is run post authentication. This means if +you have a route you want to authorise you can add the following to you ReRoute configuration. + +```json +"RouteClaimsRequirement": { + "UserType": "registered" +}, +``` + +In this example when the authorisation middleware is called Ocelot will check to see +if the user has the claim type UserType and if the value of that claim is registered. +If it isn't then the user will not be authorised and the response will be 403 forbidden. + +## Claims Tranformation + +Ocelot allows the user to access claims and transform them into headers, query string +parameters and other claims. This is only available once a user has been authenticated. + +After the user is authenticated we run the claims to claims transformation middleware. +This allows the user to transform claims before the authorisation middleware is called. +After the user is authorised first we call the claims to headers middleware and Finally +the claims to query strig parameters middleware. + +The syntax for performing the transforms is the same for each proces. In the ReRoute +configuration a json dictionary is added with a specific name either AddClaimsToRequest, +AddHeadersToRequest, AddQueriesToRequest. + +Note I'm not a hotshot programmer so have no idea if this syntax is good.. + +Within this dictionary the entries specify how Ocelot should transform things! +The key to the dictionary is going to become the key of either a claim, header +or query parameter. + +The value of the entry is parsed to logic that will perform the transform. First of +all a dictionary accessor is specified e.g. Claims[CustomerId]. This means we want +to access the claims and get the CustomerId claim type. Next is a greater than (>) +symbol which is just used to split the string. The next entry is either value or value with +and indexer. If value is specifed Ocelot will just take the value and add it to the +transform. If the value has an indexer Ocelot will look for a delimiter which is provided +after another greater than symbol. Ocelot will then split the value on the delimiter +and add whatever was at the index requested to the transform. + +#### Claims to Claims Tranformation + +Below is an example configuration that will transforms claims to claims + +```json + "AddClaimsToRequest": { + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" +}, +``` + +This shows a transforms where Ocelot looks at the users sub claim and transforms it into +UserType and UserId claims. Assuming the sub looks like this "usertypevalue|useridvalue". + +#### Claims to Headers Tranformation + +Below is an example configuration that will transforms claims to headers + +```json +"AddHeadersToRequest": { + "CustomerId": "Claims[sub] > value[1] > |" +}, +``` + +This shows a transform where Ocelot looks at the users sub claim and trasnforms it into a +CustomerId header. Assuming the sub looks like this "usertypevalue|useridvalue". + +#### Claims to Query String Parameters Tranformation + +Below is an example configuration that will transforms claims to query string parameters + +```json +"AddQueriesToRequest": { + "LocationId": "Claims[LocationId] > value", +}, +``` + +This shows a transform where Ocelot looks at the users LocationId claim and add its as +a query string parameter to be forwarded onto the downstream service. + +## Quality of Service + +Ocelot supports one QoS capability at the current time. You can set on a per ReRoute basis if you +want to use a circuit breaker when making requests to a downstream service. This uses the an awesome +.NET library called Polly check them out [here](https://github.com/App-vNext/Polly). + +Add the following section to a ReRoute configuration. + +```json +"QoSOptions": { + "ExceptionsAllowedBeforeBreaking":3, + "DurationOfBreak":5, + "TimeoutValue":5000 +} +``` + +You must set a number greater than 0 against ExceptionsAllowedBeforeBreaking for this rule to be +implemented. Duration of break is how long the circuit breaker will stay open for after it is tripped. +TimeoutValue means ff a request takes more than 5 seconds it will automatically be timed out. + +If you do not add a QoS section QoS will not be used. + +## RequestId / CorrelationId + +Ocelot supports a client sending a request id in the form of a header. If set Ocelot will +use the requestid for logging as soon as it becomes available in the middleware pipeline. +Ocelot will also forward the request id with the specified header to the downstream service. +I'm not sure if have this spot on yet in terms of the pipeline order becasue there are a few logs +that don't get the users request id at the moment and ocelot just logs not set for request id +which sucks. You can still get the framework request id in the logs if you set +IncludeScopes true in your logging config. This can then be used to match up later logs that do +have an OcelotRequestId. + +In order to use the requestid feature in your ReRoute configuration add this setting + +```json +"RequestIdKey": "OcRequestId" +``` + +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. + +```json +"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 +the [CacheManager](http://cachemanager.net/) project. This is an amazing project +that is solving a lot of caching problems. I would reccomend using this package to +cache with Ocelot. If you look at the example [here](https://github.com/TomPallister/Ocelot/blob/develop/test/Ocelot.ManualTest/Startup.cs) +you can see how the cache manager is setup and then passed into the Ocelot +AddOcelotOutputCaching configuration method. You can use any settings supported by +the CacheManager package and just pass them in. + +Anyway Ocelot currently supports caching on the URL of the downstream service +and setting a TTL in seconds to expire the cache. More to come! + +In orde to use caching on a route in your ReRoute configuration add this setting. + +```json +"FileCacheOptions": { "TtlSeconds": 15 } +``` + +In this example ttl seconds is set to 15 which means the cache will expire after 15 seconds. + +## Ocelot Middleware injection and overrides + +Warning use with caution. If you are seeing any exceptions or strange behavior in your middleware +pipeline and you are using any of the following. Remove them and try again! + +When setting up Ocelot in your Startup.cs you can provide some additonal middleware +and override middleware. This is done as follos. + +```csharp +var configuration = new OcelotMiddlewareConfiguration +{ + PreErrorResponderMiddleware = async (ctx, next) => + { + await next.Invoke(); + } +}; + +app.UseOcelot(configuration); +``` + +In the example above the provided function will run before the first piece of Ocelot middleware. +This allows a user to supply any behaviours they want before and after the Ocelot pipeline has run. +This means you can break everything so use at your own pleasure! + +The user can set functions against the following. + ++ PreErrorResponderMiddleware - Already explained above. + ++ PreAuthenticationMiddleware - This allows the user to run pre authentication logic and then call +Ocelot's authentication middleware. + ++ AuthenticationMiddleware - This overrides Ocelots authentication middleware. + ++ PreAuthorisationMiddleware - This allows the user to run pre authorisation logic and then call +Ocelot's authorisation middleware. + ++ AuthorisationMiddleware - This overrides Ocelots authorisation middleware. + ++ PreQueryStringBuilderMiddleware - This alows the user to manipulate the query string on the +http request before it is passed to Ocelots request creator. + +Obviously you can just add middleware as normal before the call to app.UseOcelot() It cannot be added +after as Ocelot does not call the next middleware. + +## Logging + +Ocelot uses the standard logging interfaces ILoggerFactory / ILogger at the moment. +This is encapsulated in IOcelotLogger / IOcelotLoggerFactory with an implementation +for the standard asp.net core logging stuff at the moment. + +There are a bunch of debugging logs in the ocelot middlewares however I think the +system probably needs more logging in the code it calls into. Other than the debugging +there is a global error handler that should catch any errors thrown and log them as errors. + +The reason for not just using bog standard framework logging is that I could not +work out how to override the request id that get's logged when setting IncludeScopes +to true for logging settings. Nicely onto the next feature. + +## Not supported + +Ocelot does not support... + ++ Chunked Encoding - Ocelot will always get the body size and return Content-Length +header. Sorry if this doesn't work for your use case! + ++ Fowarding a host header - The host header that you send to Ocelot will not be +forwarded to the downstream service. Obviously this would break everything :( + +## Things that are currently annoying me + ++ The ReRoute configuration object is too large. + ++ The base OcelotMiddleware lets you access things that are going to be null +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. + +## Coming up + +You can see what we are working on [here](https://github.com/TomPallister/Ocelot/projects/1) + + + diff --git a/ReleaseNotes.md b/ReleaseNotes.md new file mode 100644 index 00000000..a647c6c3 --- /dev/null +++ b/ReleaseNotes.md @@ -0,0 +1 @@ +No issues closed since last release \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 69ddc117..00000000 --- a/appveyor.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: 1.0.{build} -configuration: -- Release -platform: Any CPU -build_script: -- build.bat -test_script: -- run-tests.bat -after_test: -- push-to-nuget.bat %appveyor_build_version%-rc1 %nugetApiKey% -cache: -- '%USERPROFILE%\.nuget\packages' \ No newline at end of file diff --git a/build-and-release-unstable.ps1 b/build-and-release-unstable.ps1 new file mode 100644 index 00000000..51c6f0d5 --- /dev/null +++ b/build-and-release-unstable.ps1 @@ -0,0 +1 @@ +./build.ps1 -target BuildAndReleaseUnstable \ No newline at end of file diff --git a/build-and-run-tests.bat b/build-and-run-tests.bat deleted file mode 100644 index 764682b6..00000000 --- a/build-and-run-tests.bat +++ /dev/null @@ -1,2 +0,0 @@ -./run-tests.bat -./build.bat \ No newline at end of file diff --git a/build-and-run-tests.ps1 b/build-and-run-tests.ps1 new file mode 100644 index 00000000..f82502e5 --- /dev/null +++ b/build-and-run-tests.ps1 @@ -0,0 +1 @@ +./build.ps1 -target RunTests \ No newline at end of file diff --git a/build.bat b/build.bat deleted file mode 100644 index 656515d4..00000000 --- a/build.bat +++ /dev/null @@ -1,8 +0,0 @@ -echo ------------------------- - -echo Building Ocelot -dotnet restore src/Ocelot -dotnet build src/Ocelot -c Release - - - diff --git a/build.cake b/build.cake new file mode 100644 index 00000000..7d096754 --- /dev/null +++ b/build.cake @@ -0,0 +1,388 @@ +#tool "nuget:?package=GitVersion.CommandLine" +#tool "nuget:?package=OpenCover" +#tool "nuget:?package=ReportGenerator" +#tool "nuget:?package=GitReleaseNotes" +#addin "nuget:?package=Cake.DoInDirectory" +#addin "nuget:?package=Cake.Json" + +// compile +var compileConfig = Argument("configuration", "Release"); +var projectJson = "./src/Ocelot/project.json"; + +// build artifacts +var artifactsDir = Directory("artifacts"); + +// unit testing +var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests"); +var unitTestAssemblies = @"./test/Ocelot.UnitTests"; + +// acceptance testing +var artifactsForAcceptanceTestsDir = artifactsDir + Directory("AcceptanceTests"); + +// integration testing +var artifactsForIntegrationTestsDir = artifactsDir + Directory("IntegrationTests"); + +// benchmark testing +var artifactsForBenchmarkTestsDir = artifactsDir + Directory("BenchmarkTests"); +var benchmarkTestAssemblies = @"./test/Ocelot.Benchmarks"; + +// packaging +var packagesDir = artifactsDir + Directory("Packages"); +var releaseNotesFile = packagesDir + File("releasenotes.md"); +var artifactsFile = packagesDir + File("artifacts.txt"); + +// unstable releases +var nugetFeedUnstableKey = EnvironmentVariable("nuget-apikey-unstable"); +var nugetFeedUnstableUploadUrl = "https://www.nuget.org/api/v2/package"; +var nugetFeedUnstableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; + +// stable releases +var tagsUrl = "https://api.github.com/repos/tompallister/ocelot/releases/tags/"; +var nugetFeedStableKey = EnvironmentVariable("nuget-apikey-stable"); +var nugetFeedStableUploadUrl = "https://www.nuget.org/api/v2/package"; +var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; + +// internal build variables - don't change these. +var releaseTag = ""; +string committedVersion = "0.0.0-dev"; +var buildVersion = committedVersion; +GitVersion versioning = null; +var nugetFeedUnstableBranchFilter = "^(develop)$|^(PullRequest/)"; + +var target = Argument("target", "Default"); + + +Information("target is " +target); +Information("Build configuration is " + compileConfig); + +Task("Default") + .IsDependentOn("Build"); + +Task("Build") + .IsDependentOn("RunTests") + .IsDependentOn("CreatePackages"); + +Task("BuildAndReleaseUnstable") + .IsDependentOn("Build") + .IsDependentOn("ReleasePackagesToUnstableFeed"); + +Task("Clean") + .Does(() => + { + if (DirectoryExists(artifactsDir)) + { + DeleteDirectory(artifactsDir, recursive:true); + } + CreateDirectory(artifactsDir); + }); + +Task("Version") + .Does(() => + { + versioning = GetNuGetVersionForCommit(); + var nugetVersion = versioning.NuGetVersion; + + Information("SemVer version number: " + nugetVersion); + + if (AppVeyor.IsRunningOnAppVeyor) + { + Information("Persisting version number..."); + PersistVersion(committedVersion, nugetVersion); + buildVersion = nugetVersion; + } + else + { + Information("We are not running on build server, so we won't persist the version number."); + } + }); + +Task("Restore") + .IsDependentOn("Clean") + .IsDependentOn("Version") + .Does(() => + { + DotNetCoreRestore("./src"); + DotNetCoreRestore("./test"); + }); + +Task("RunUnitTests") + .IsDependentOn("Restore") + .Does(() => + { + var buildSettings = new DotNetCoreTestSettings + { + Configuration = compileConfig, + }; + + EnsureDirectoryExists(artifactsForUnitTestsDir); + DotNetCoreTest(unitTestAssemblies, buildSettings); + }); + +Task("RunAcceptanceTests") + .IsDependentOn("Restore") + .Does(() => + { + var buildSettings = new DotNetCoreTestSettings + { + Configuration = "Debug", //acceptance test config is hard-coded for debug + }; + + EnsureDirectoryExists(artifactsForAcceptanceTestsDir); + + DoInDirectory("test/Ocelot.AcceptanceTests", () => + { + DotNetCoreTest(".", buildSettings); + }); + + }); + + Task("RunIntegrationTests") + .IsDependentOn("Restore") + .Does(() => + { + var buildSettings = new DotNetCoreTestSettings + { + Configuration = "Debug", //int test config is hard-coded for debug + }; + + EnsureDirectoryExists(artifactsForIntegrationTestsDir); + + DoInDirectory("test/Ocelot.IntegrationTests", () => + { + DotNetCoreTest(".", buildSettings); + }); + + }); + +Task("RunBenchmarkTests") + .IsDependentOn("Restore") + .Does(() => + { + var buildSettings = new DotNetCoreRunSettings + { + Configuration = compileConfig, + }; + + EnsureDirectoryExists(artifactsForBenchmarkTestsDir); + + DoInDirectory(benchmarkTestAssemblies, () => + { + DotNetCoreRun(".", "", buildSettings); + }); + }); + +Task("RunTests") + .IsDependentOn("RunUnitTests") + .IsDependentOn("RunAcceptanceTests") + .IsDependentOn("RunIntegrationTests") + .Does(() => + { + }); + +Task("CreatePackages") + .Does(() => + { + EnsureDirectoryExists(packagesDir); + + GenerateReleaseNotes(releaseNotesFile); + + var settings = new DotNetCorePackSettings + { + OutputDirectory = packagesDir, + NoBuild = true + }; + + DotNetCorePack(projectJson, settings); + + System.IO.File.WriteAllLines(artifactsFile, new[]{ + "nuget:Ocelot." + buildVersion + ".nupkg", + "nugetSymbols:Ocelot." + buildVersion + ".symbols.nupkg", + "releaseNotes:releasenotes.md" + }); + + if (AppVeyor.IsRunningOnAppVeyor) + { + var path = packagesDir.ToString() + @"/**/*"; + + foreach (var file in GetFiles(path)) + { + AppVeyor.UploadArtifact(file.FullPath); + } + } + }); + +Task("ReleasePackagesToUnstableFeed") + .IsDependentOn("CreatePackages") + .Does(() => + { + if (ShouldPublishToUnstableFeed(nugetFeedUnstableBranchFilter, versioning.BranchName)) + { + PublishPackages(packagesDir, artifactsFile, nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl); + } + }); + +Task("EnsureStableReleaseRequirements") + .Does(() => + { + if (!AppVeyor.IsRunningOnAppVeyor) + { + throw new Exception("Stable release should happen via appveyor"); + } + + var isTag = + AppVeyor.Environment.Repository.Tag.IsTag && + !string.IsNullOrWhiteSpace(AppVeyor.Environment.Repository.Tag.Name); + + if (!isTag) + { + throw new Exception("Stable release should happen from a published GitHub release"); + } + }); + +Task("UpdateVersionInfo") + .IsDependentOn("EnsureStableReleaseRequirements") + .Does(() => + { + releaseTag = AppVeyor.Environment.Repository.Tag.Name; + AppVeyor.UpdateBuildVersion(releaseTag); + }); + +Task("DownloadGitHubReleaseArtifacts") + .IsDependentOn("UpdateVersionInfo") + .Does(() => + { + EnsureDirectoryExists(packagesDir); + + var releaseUrl = tagsUrl + releaseTag; + var assets_url = ParseJson(GetResource(releaseUrl)) + .GetValue("assets_url") + .Value(); + + foreach(var asset in DeserializeJson(GetResource(assets_url))) + { + var file = packagesDir + File(asset.Value("name")); + Information("Downloading " + file); + DownloadFile(asset.Value("browser_download_url"), file); + } + }); + +Task("ReleasePackagesToStableFeed") + .IsDependentOn("DownloadGitHubReleaseArtifacts") + .Does(() => + { + PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); + }); + +Task("Release") + .IsDependentOn("ReleasePackagesToStableFeed"); + +RunTarget(target); + +/// Gets nuique nuget version for this commit +private GitVersion GetNuGetVersionForCommit() +{ + GitVersion(new GitVersionSettings{ + UpdateAssemblyInfo = false, + OutputType = GitVersionOutput.BuildServer + }); + + return GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json }); +} + +/// Updates project version in all of our projects +private void PersistVersion(string committedVersion, string newVersion) +{ + Information(string.Format("We'll search all project.json files for {0} and replace with {1}...", committedVersion, newVersion)); + + var projectJsonFiles = GetFiles("./**/project.json"); + + foreach(var projectJsonFile in projectJsonFiles) + { + var file = projectJsonFile.ToString(); + + Information(string.Format("Updating {0}...", file)); + + var updatedProjectJson = System.IO.File.ReadAllText(file) + .Replace(committedVersion, newVersion); + + System.IO.File.WriteAllText(file, updatedProjectJson); + } +} + +/// generates release notes based on issues closed in GitHub since the last release +private void GenerateReleaseNotes(ConvertableFilePath file) +{ + if (!IsRunningOnWindows()) + { + Warning("We can't generate release notes as we're not running on Windows."); + return; + } + + Information("Generating release notes at " + file); + + var releaseNotesExitCode = StartProcess( + @"tools/GitReleaseNotes/tools/gitreleasenotes.exe", + new ProcessSettings { Arguments = ". /o " + file }); + + if (string.IsNullOrEmpty(System.IO.File.ReadAllText(file))) + { + System.IO.File.WriteAllText(file, "No issues closed since last release"); + } + + if (releaseNotesExitCode != 0) + { + throw new Exception("Failed to generate release notes"); + } +} + +/// Publishes code and symbols packages to nuget feed, based on contents of artifacts file +private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFilePath artifactsFile, string feedApiKey, string codeFeedUrl, string symbolFeedUrl) +{ + var artifacts = System.IO.File + .ReadAllLines(artifactsFile) + .Select(l => l.Split(':')) + .ToDictionary(v => v[0], v => v[1]); + + var codePackage = packagesDir + File(artifacts["nuget"]); + + Information("Pushing package " + codePackage); + NuGetPush( + codePackage, + new NuGetPushSettings { + ApiKey = feedApiKey, + Source = codeFeedUrl + }); +} + +/// gets the resource from the specified url +private string GetResource(string url) +{ + Information("Getting resource from " + url); + + var assetsRequest = System.Net.WebRequest.CreateHttp(url); + assetsRequest.Method = "GET"; + assetsRequest.Accept = "application/vnd.github.v3+json"; + assetsRequest.UserAgent = "BuildScript"; + + using (var assetsResponse = assetsRequest.GetResponse()) + { + var assetsStream = assetsResponse.GetResponseStream(); + var assetsReader = new StreamReader(assetsStream); + return assetsReader.ReadToEnd(); + } +} + +private bool ShouldPublishToUnstableFeed(string filter, string branchName) +{ + var regex = new System.Text.RegularExpressions.Regex(filter); + var publish = regex.IsMatch(branchName); + if (publish) + { + Information("Branch " + branchName + " will be published to the unstable feed"); + } + else + { + Information("Branch " + branchName + " will not be published to the unstable feed"); + } + return publish; +} \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 00000000..44de5793 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,189 @@ +########################################################################## +# This is the Cake bootstrapper script for PowerShell. +# This file was downloaded from https://github.com/cake-build/resources +# Feel free to change this file to fit your needs. +########################################################################## + +<# + +.SYNOPSIS +This is a Powershell script to bootstrap a Cake build. + +.DESCRIPTION +This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) +and execute your Cake build script with the parameters you provide. + +.PARAMETER Script +The build script to execute. +.PARAMETER Target +The build script target to run. +.PARAMETER Configuration +The build configuration to use. +.PARAMETER Verbosity +Specifies the amount of information to be displayed. +.PARAMETER Experimental +Tells Cake to use the latest Roslyn release. +.PARAMETER WhatIf +Performs a dry run of the build script. +No tasks will be executed. +.PARAMETER Mono +Tells Cake to use the Mono scripting engine. +.PARAMETER SkipToolPackageRestore +Skips restoring of packages. +.PARAMETER ScriptArgs +Remaining arguments are added here. + +.LINK +http://cakebuild.net + +#> + +[CmdletBinding()] +Param( + [string]$Script = "build.cake", + [string]$Target = "Default", + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] + [string]$Verbosity = "Verbose", + [switch]$Experimental, + [Alias("DryRun","Noop")] + [switch]$WhatIf, + [switch]$Mono, + [switch]$SkipToolPackageRestore, + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$ScriptArgs +) + +[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null +function MD5HashFile([string] $filePath) +{ + if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) + { + return $null + } + + [System.IO.Stream] $file = $null; + [System.Security.Cryptography.MD5] $md5 = $null; + try + { + $md5 = [System.Security.Cryptography.MD5]::Create() + $file = [System.IO.File]::OpenRead($filePath) + return [System.BitConverter]::ToString($md5.ComputeHash($file)) + } + finally + { + if ($file -ne $null) + { + $file.Dispose() + } + } +} + +Write-Host "Preparing to run build script..." + +if(!$PSScriptRoot){ + $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent +} + +$TOOLS_DIR = Join-Path $PSScriptRoot "tools" +$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" +$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" +$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" +$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" +$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" + +# Should we use mono? +$UseMono = ""; +if($Mono.IsPresent) { + Write-Verbose -Message "Using the Mono based scripting engine." + $UseMono = "-mono" +} + +# Should we use the new Roslyn? +$UseExperimental = ""; +if($Experimental.IsPresent -and !($Mono.IsPresent)) { + Write-Verbose -Message "Using experimental version of Roslyn." + $UseExperimental = "-experimental" +} + +# Is this a dry run? +$UseDryRun = ""; +if($WhatIf.IsPresent) { + $UseDryRun = "-dryrun" +} + +# Make sure tools folder exists +if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { + Write-Verbose -Message "Creating tools directory..." + New-Item -Path $TOOLS_DIR -Type directory | out-null +} + +# Make sure that packages.config exist. +if (!(Test-Path $PACKAGES_CONFIG)) { + Write-Verbose -Message "Downloading packages.config..." + try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { + Throw "Could not download packages.config." + } +} + +# Try find NuGet.exe in path if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Trying to find nuget.exe in PATH..." + $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } + $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 + if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { + Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." + $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName + } +} + +# Try download NuGet.exe if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Downloading NuGet.exe..." + try { + (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) + } catch { + Throw "Could not download NuGet.exe." + } +} + +# Save nuget.exe path to environment to be available to child processed +$ENV:NUGET_EXE = $NUGET_EXE + +# Restore tools from NuGet? +if(-Not $SkipToolPackageRestore.IsPresent) { + Push-Location + Set-Location $TOOLS_DIR + + # Check for changes in packages.config and remove installed tools if true. + [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) + if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or + ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { + Write-Verbose -Message "Missing or changed package.config hash..." + Remove-Item * -Recurse -Exclude packages.config,nuget.exe + } + + Write-Verbose -Message "Restoring tools from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occured while restoring NuGet tools." + } + else + { + $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" + } + Write-Verbose -Message ($NuGetOutput | out-string) + Pop-Location +} + +# Make sure that Cake has been installed. +if (!(Test-Path $CAKE_EXE)) { + Throw "Could not find Cake.exe at $CAKE_EXE" +} + +# Start Cake +Write-Host "Running build script..." +Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" +exit $LASTEXITCODE \ No newline at end of file diff --git a/build.readme.md b/build.readme.md new file mode 100644 index 00000000..fccff4d5 --- /dev/null +++ b/build.readme.md @@ -0,0 +1,22 @@ +#1. Overview + +This document summarises the build and release process for the project. The build scripts are written using [Cake](http://cakebuild.net/), and are defined in `./build.cake`. The scripts have been designed to be run by either developers locally or by a build server (currently [AppVeyor](https://www.appveyor.com/)), with minimal logic defined in the build server itself. + +#2. Building + * You'll generally want to run the `./build.ps1` script. This will compile, run unit and acceptance tests and build the output packages locally. Output will got to the `./artifacts` directory. + * You can view the current commit's [SemVer](http://semver.org/) build information by running `./version.ps1`. + * The other `./*.ps1` scripts perform subsets of the build process, if you don't want to run the full build. + * The release process works best with GitFlow branching; this allows us to publish every development commit to an unstable feed with a unique SemVer version, and then choose when to release to a stable feed. + +#3. Release process +This section defines the release process for the maintainers of the project. + * Merge pull requests to the `release` branch. + * Every commit pushed to the Origin repo will kick off the [ocelot-build](https://ci.appveyor.com/project/binarymash/ocelot) project in AppVeyor. This performs the same tasks as the command line build, and in addition pushes the packages to the unstable nuget feed. + * When you're ready for a release, create a release branch. You'll probably want to update the committed `./ReleaseNotes.md` based on the contents of the equivalent file in the `./artifacts` directory. + * When the `release` branch has built successfully in Appveyor, select the build and then Deploy to the `GitHub Release` environment. This will create a new release in GitHub. + * In Github, navigate to the [release](https://github.com/binarymash/Ocelot/releases). Modify the release name and tag as desired. + * When you're ready, publish the release. This will tag the commit with the specified release number. + * The [ocelot-release](https://ci.appveyor.com/project/binarymash/ocelot-wtaj9) project will detect the newly created tag and kick off the release process. This will download the artifacts from GitHub, and publish the packages to the stable nuget feed. + * When you have a final stable release build, merge the `release` branch into `master` and `develop`. Deploy the master branch to github and following the full release process as described above. Don't forget to uncheck the "This is a pre-release" checkbox in GitHub before publishing. + * Note - because the release builds are initiated by tagging a commit, if for some reason a release build fails in AppVeyor you'll need to delete the tag from the repo and republish the release in GitHub. + diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..04731adf --- /dev/null +++ b/build.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash + +########################################################################## +# This is the Cake bootstrapper script for Linux and OS X. +# This file was downloaded from https://github.com/cake-build/resources +# Feel free to change this file to fit your needs. +########################################################################## + +# Define directories. +SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +TOOLS_DIR=$SCRIPT_DIR/tools +NUGET_EXE=$TOOLS_DIR/nuget.exe +CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe +PACKAGES_CONFIG=$TOOLS_DIR/packages.config +PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum + +# Define md5sum or md5 depending on Linux/OSX +MD5_EXE= +if [[ "$(uname -s)" == "Darwin" ]]; then + MD5_EXE="md5 -r" +else + MD5_EXE="md5sum" +fi + +# Define default arguments. +SCRIPT="build.cake" +TARGET="Default" +CONFIGURATION="Release" +VERBOSITY="verbose" +DRYRUN= +SHOW_VERSION=false +SCRIPT_ARGUMENTS=() + +# Parse arguments. +for i in "$@"; do + case $1 in + -s|--script) SCRIPT="$2"; shift ;; + -t|--target) TARGET="$2"; shift ;; + -c|--configuration) CONFIGURATION="$2"; shift ;; + -v|--verbosity) VERBOSITY="$2"; shift ;; + -d|--dryrun) DRYRUN="-dryrun" ;; + --version) SHOW_VERSION=true ;; + --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;; + *) SCRIPT_ARGUMENTS+=("$1") ;; + esac + shift +done + +# Make sure the tools folder exist. +if [ ! -d "$TOOLS_DIR" ]; then + mkdir "$TOOLS_DIR" +fi + +# Make sure that packages.config exist. +if [ ! -f "$TOOLS_DIR/packages.config" ]; then + echo "Downloading packages.config..." + curl -Lsfo "$TOOLS_DIR/packages.config" http://cakebuild.net/download/bootstrapper/packages + if [ $? -ne 0 ]; then + echo "An error occured while downloading packages.config." + exit 1 + fi +fi + +# Download NuGet if it does not exist. +if [ ! -f "$NUGET_EXE" ]; then + echo "Downloading NuGet..." + curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe + if [ $? -ne 0 ]; then + echo "An error occured while downloading nuget.exe." + exit 1 + fi +fi + +# Restore tools from NuGet. +pushd "$TOOLS_DIR" >/dev/null +if [ ! -f "$PACKAGES_CONFIG_MD5" ] || [ "$( cat "$PACKAGES_CONFIG_MD5" | sed 's/\r$//' )" != "$( $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' )" ]; then + find . -type d ! -name . | xargs rm -rf +fi + +mono "$NUGET_EXE" install -ExcludeVersion +if [ $? -ne 0 ]; then + echo "Could not restore NuGet packages." + exit 1 +fi + +$MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' >| "$PACKAGES_CONFIG_MD5" + +popd >/dev/null + +# Make sure that Cake has been installed. +if [ ! -f "$CAKE_EXE" ]; then + echo "Could not find Cake.exe at '$CAKE_EXE'." + exit 1 +fi + +# Start Cake +if $SHOW_VERSION; then + exec mono "$CAKE_EXE" -version +else + exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY -configuration=$CONFIGURATION -target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}" +fi \ No newline at end of file diff --git a/configuration-explanation.txt b/configuration-explanation.txt index 898be89f..d7b4077f 100644 --- a/configuration-explanation.txt +++ b/configuration-explanation.txt @@ -1,12 +1,21 @@ { "ReRoutes": [ { - # The url we are forwarding the request to, ocelot will not add a trailing slash - "DownstreamTemplate": "http://somehost.com/identityserverexample", - # The path we are listening on for this re route, Ocelot will add a trailing slash to - # this property. Then when a request is made Ocelot makes sure a trailing slash is added - # to that so everything matches - "UpstreamTemplate": "/identityserverexample", + # The downstream path we are forwarding the request to, ocelot will not add a trailing slash. + # Ocelot replaces any placeholders {etc} with matched values from the incoming request. + "DownstreamPathTemplate": "/identityserverexample/{someid}/something", + # The scheme you want Ocelot to use when making the downstream request + "DownstreamScheme": "https", + # The port you want Ocelot to use when making the downstream request, will default to + # scheme if nothing set + "DownstreamPort": 80, + # The host address of the downstream service, should not have a trailing slash or scheme + # if there is a trailing slash Ocelot will remove it. + "DownstreamHost" "localhost" + # The path template we are listening on for this re route, Ocelot will add a trailing + # slash to this property. Then when a request is made Ocelot makes sure a trailing + # slash is added, so everything matches + "UpstreamPathTemplate": "/identityserverexample", # The method we are listening for on this re route "UpstreamHttpMethod": "Get", # Only support identity server at the moment @@ -71,12 +80,24 @@ # the caching a lot. "FileCacheOptions": { "TtlSeconds": 15 }, # The value of this is used when matching the upstream template to an upstream url. - "ReRouteIsCaseSensitive": false + "ReRouteIsCaseSensitive": false, + # Tells Ocelot the name of the service it is looking when making requests to service discovery + # for hosts and ports + "ServiceName": "product" + # Tells Ocelot which load balancer to use when making downstream requests. + "LoadBalancer": "RoundRobin" }, # 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", + # If set Ocelot will try and use service discovery to locate downstream hosts and ports + "ServiceDiscoveryProvider": + { + "Provider":"Consul", + "Host":"localhost", + "Port":8500 + } } } \ No newline at end of file diff --git a/configuration.json b/configuration.json new file mode 100755 index 00000000..3f39532c --- /dev/null +++ b/configuration.json @@ -0,0 +1 @@ +{"ReRoutes":[],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"AdministrationPath":"/administration"}} \ No newline at end of file diff --git a/configuration.yaml b/configuration.yaml new file mode 100755 index 00000000..2e47e77d --- /dev/null +++ b/configuration.yaml @@ -0,0 +1,3 @@ +Routes: +- Downstream: http://localhost:51879/ + Upstream: /heee diff --git a/ocelot.postman_collection.json b/ocelot.postman_collection.json new file mode 100644 index 00000000..1da66b38 --- /dev/null +++ b/ocelot.postman_collection.json @@ -0,0 +1,117 @@ +{ + "id": "23a49657-e24b-b967-7ec0-943ff1368680", + "name": "Ocelot Admin", + "description": "", + "order": [ + "59162efa-27ce-c230-f523-81d31ead603d", + "e0defe09-c1b2-9e95-8237-67df4bbab284", + "30007c41-565c-5b87-ea34-42170dd386d7" + ], + "folders": [], + "timestamp": 1488042899799, + "owner": "212120", + "public": false, + "requests": [ + { + "id": "30007c41-565c-5b87-ea34-42170dd386d7", + "headers": "Authorization: Bearer {{AccessToken}}\n", + "url": "http://localhost:5000/admin/configuration", + "preRequestScript": null, + "pathVariables": {}, + "method": "GET", + "data": null, + "dataMode": "params", + "version": 2, + "tests": null, + "currentHelper": "normal", + "helperAttributes": "{}", + "time": 1487515927978, + "name": "POST http://localhost:5000/admin/configuration", + "description": "", + "collectionId": "23a49657-e24b-b967-7ec0-943ff1368680", + "responses": [], + "isFromCollection": true, + "collectionRequestId": "59162efa-27ce-c230-f523-81d31ead603d" + }, + { + "id": "59162efa-27ce-c230-f523-81d31ead603d", + "headers": "Authorization: Bearer {{AccessToken}}\nContent-Type: application/json\n", + "url": "http://localhost:5000/admin/configuration", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [], + "dataMode": "raw", + "version": 2, + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "time": 1488044268493, + "name": "GET http://localhost:5000/admin/configuration", + "description": "", + "collectionId": "23a49657-e24b-b967-7ec0-943ff1368680", + "responses": [], + "rawModeData": "{\n \"reRoutes\": [\n {\n \"downstreamPathTemplate\": \"/\",\n \"upstreamPathTemplate\": \"/identityserverexample\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": \"IdentityServer\",\n \"providerRootUrl\": \"http://localhost:52888\",\n \"scopeName\": \"api\",\n \"requireHttps\": false,\n \"additionalScopes\": [\n \"openid\",\n \"offline_access\"\n ],\n \"scopeSecret\": \"secret\"\n },\n \"addHeadersToRequest\": {\n \"CustomerId\": \"Claims[CustomerId] > value\",\n \"LocationId\": \"Claims[LocationId] > value\",\n \"UserId\": \"Claims[sub] > value[1] > |\",\n \"UserType\": \"Claims[sub] > value[0] > |\"\n },\n \"addClaimsToRequest\": {\n \"CustomerId\": \"Claims[CustomerId] > value\",\n \"LocationId\": \"Claims[LocationId] > value\",\n \"UserId\": \"Claims[sub] > value[1] > |\",\n \"UserType\": \"Claims[sub] > value[0] > |\"\n },\n \"routeClaimsRequirement\": {\n \"UserType\": \"registered\"\n },\n \"addQueriesToRequest\": {\n \"CustomerId\": \"Claims[CustomerId] > value\",\n \"LocationId\": \"Claims[LocationId] > value\",\n \"UserId\": \"Claims[sub] > value[1] > |\",\n \"UserType\": \"Claims[sub] > value[0] > |\"\n },\n \"requestIdKey\": \"OcRequestId\",\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"localhost\",\n \"downstreamPort\": 52876,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/\",\n \"upstreamPathTemplate\": \"/posts\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"www.bbc.co.uk\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts/{postId}/comments\",\n \"upstreamPathTemplate\": \"/posts/{postId}/comments\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/comments\",\n \"upstreamPathTemplate\": \"/comments\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts\",\n \"upstreamPathTemplate\": \"/posts\",\n \"upstreamHttpMethod\": \"Post\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamHttpMethod\": \"Put\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamHttpMethod\": \"Patch\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamPathTemplate\": \"/posts/{postId}\",\n \"upstreamHttpMethod\": \"Delete\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/products\",\n \"upstreamPathTemplate\": \"/products\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/products/{productId}\",\n \"upstreamPathTemplate\": \"/products/{productId}\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 0,\n \"durationOfBreak\": 0,\n \"timeoutValue\": 0\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/products\",\n \"upstreamPathTemplate\": \"/products\",\n \"upstreamHttpMethod\": \"Post\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 0\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"products20161126090340.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/products/{productId}\",\n \"upstreamPathTemplate\": \"/products/{productId}\",\n \"upstreamHttpMethod\": \"Put\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"products20161126090340.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/products/{productId}\",\n \"upstreamPathTemplate\": \"/products/{productId}\",\n \"upstreamHttpMethod\": \"Delete\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"products20161126090340.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/customers\",\n \"upstreamPathTemplate\": \"/customers\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"customers20161126090811.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/customers/{customerId}\",\n \"upstreamPathTemplate\": \"/customers/{customerId}\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"customers20161126090811.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/customers\",\n \"upstreamPathTemplate\": \"/customers\",\n \"upstreamHttpMethod\": \"Post\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"customers20161126090811.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/customers/{customerId}\",\n \"upstreamPathTemplate\": \"/customers/{customerId}\",\n \"upstreamHttpMethod\": \"Put\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"customers20161126090811.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/api/customers/{customerId}\",\n \"upstreamPathTemplate\": \"/customers/{customerId}\",\n \"upstreamHttpMethod\": \"Delete\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"customers20161126090811.azurewebsites.net\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n },\n {\n \"downstreamPathTemplate\": \"/posts\",\n \"upstreamPathTemplate\": \"/posts/\",\n \"upstreamHttpMethod\": \"Get\",\n \"authenticationOptions\": {\n \"provider\": null,\n \"providerRootUrl\": null,\n \"scopeName\": null,\n \"requireHttps\": false,\n \"additionalScopes\": [],\n \"scopeSecret\": null\n },\n \"addHeadersToRequest\": {},\n \"addClaimsToRequest\": {},\n \"routeClaimsRequirement\": {},\n \"addQueriesToRequest\": {},\n \"requestIdKey\": null,\n \"fileCacheOptions\": {\n \"ttlSeconds\": 15\n },\n \"reRouteIsCaseSensitive\": false,\n \"serviceName\": null,\n \"downstreamScheme\": \"http\",\n \"downstreamHost\": \"jsonplaceholder.typicode.com\",\n \"downstreamPort\": 80,\n \"qoSOptions\": {\n \"exceptionsAllowedBeforeBreaking\": 3,\n \"durationOfBreak\": 10,\n \"timeoutValue\": 5000\n },\n \"loadBalancer\": null\n }\n ],\n \"globalConfiguration\": {\n \"requestIdKey\": \"OcRequestId\",\n \"serviceDiscoveryProvider\": {\n \"provider\": null,\n \"host\": null,\n \"port\": 0\n },\n \"administrationPath\": \"/admin\"\n }\n}" + }, + { + "id": "e0defe09-c1b2-9e95-8237-67df4bbab284", + "headers": "", + "url": "http://localhost:5000/admin/connect/token", + "preRequestScript": null, + "pathVariables": {}, + "method": "POST", + "data": [ + { + "key": "client_id", + "value": "admin", + "type": "text", + "enabled": true + }, + { + "key": "client_secret", + "value": "secret", + "type": "text", + "enabled": true + }, + { + "key": "scope", + "value": "admin", + "type": "text", + "enabled": true + }, + { + "key": "username", + "value": "admin", + "type": "text", + "enabled": true + }, + { + "key": "password", + "value": "secret", + "type": "text", + "enabled": true + }, + { + "key": "grant_type", + "value": "password", + "type": "text", + "enabled": true + } + ], + "dataMode": "params", + "version": 2, + "tests": "var jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"AccessToken\", jsonData.access_token);\npostman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);", + "currentHelper": "normal", + "helperAttributes": "{}", + "time": 1487515922748, + "name": "POST http://localhost:5000/admin/connect/token", + "description": "", + "collectionId": "23a49657-e24b-b967-7ec0-943ff1368680", + "responses": [], + "rawModeData": null, + "descriptionFormat": null, + "isFromCollection": true, + "collectionRequestId": "e23e29a1-6abb-abd3-141a-f2202e3f582b" + } + ] +} \ No newline at end of file diff --git a/push-to-nuget.bat b/push-to-nuget.bat deleted file mode 100644 index 7200d09c..00000000 --- a/push-to-nuget.bat +++ /dev/null @@ -1,11 +0,0 @@ -echo ------------------------- - -echo Packing Ocelot Version %1 -nuget pack .\Ocelot.nuspec -version %1 - -echo Publishing Ocelot - nuget push Ocelot.%1.nupkg -ApiKey %2 -Source https://www.nuget.org/api/v2/package - - - - diff --git a/release.ps1 b/release.ps1 new file mode 100644 index 00000000..6cf4c66b --- /dev/null +++ b/release.ps1 @@ -0,0 +1 @@ +./build.ps1 -target Release \ No newline at end of file diff --git a/run-acceptance-tests.ps1 b/run-acceptance-tests.ps1 new file mode 100644 index 00000000..480e1d4c --- /dev/null +++ b/run-acceptance-tests.ps1 @@ -0,0 +1 @@ +./build -target RunAcceptanceTests \ No newline at end of file diff --git a/run-benchmarks.bat b/run-benchmarks.bat deleted file mode 100644 index 1376f17a..00000000 --- a/run-benchmarks.bat +++ /dev/null @@ -1,15 +0,0 @@ -echo ------------------------- - -echo Running Ocelot.Benchmarks - -cd test/Ocelot.Benchmarks - -dotnet restore - -dotnet run - -cd ../../ - - - - diff --git a/run-benchmarks.ps1 b/run-benchmarks.ps1 new file mode 100644 index 00000000..e05490fd --- /dev/null +++ b/run-benchmarks.ps1 @@ -0,0 +1 @@ +./build.ps1 -target RunBenchmarkTests \ No newline at end of file diff --git a/run-tests.bat b/run-tests.bat deleted file mode 100644 index ae4856fc..00000000 --- a/run-tests.bat +++ /dev/null @@ -1,17 +0,0 @@ -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 diff --git a/run-unit-tests.ps1 b/run-unit-tests.ps1 new file mode 100644 index 00000000..0e6a91bd --- /dev/null +++ b/run-unit-tests.ps1 @@ -0,0 +1 @@ +./build.ps1 -target RunUnitTests \ No newline at end of file diff --git a/src/Ocelot/Authentication/Handler/Creator/AuthenticationHandlerCreator.cs b/src/Ocelot/Authentication/Handler/Creator/AuthenticationHandlerCreator.cs index 82765d21..67f2ebbd 100644 --- a/src/Ocelot/Authentication/Handler/Creator/AuthenticationHandlerCreator.cs +++ b/src/Ocelot/Authentication/Handler/Creator/AuthenticationHandlerCreator.cs @@ -12,18 +12,18 @@ namespace Ocelot.Authentication.Handler.Creator /// public class AuthenticationHandlerCreator : IAuthenticationHandlerCreator { - public Response CreateIdentityServerAuthenticationHandler(IApplicationBuilder app, AuthenticationOptions authOptions) + public Response Create(IApplicationBuilder app, AuthenticationOptions authOptions) { var builder = app.New(); builder.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions { Authority = authOptions.ProviderRootUrl, - ScopeName = authOptions.ScopeName, + ApiName = authOptions.ScopeName, RequireHttpsMetadata = authOptions.RequireHttps, - AdditionalScopes = authOptions.AdditionalScopes, + AllowedScopes = authOptions.AdditionalScopes, SupportedTokens = SupportedTokens.Both, - ScopeSecret = authOptions.ScopeSecret + ApiSecret = authOptions.ScopeSecret }); var authenticationNext = builder.Build(); diff --git a/src/Ocelot/Authentication/Handler/Creator/IAuthenticationHandlerCreator.cs b/src/Ocelot/Authentication/Handler/Creator/IAuthenticationHandlerCreator.cs index 6baa0385..9d92c81d 100644 --- a/src/Ocelot/Authentication/Handler/Creator/IAuthenticationHandlerCreator.cs +++ b/src/Ocelot/Authentication/Handler/Creator/IAuthenticationHandlerCreator.cs @@ -8,6 +8,6 @@ namespace Ocelot.Authentication.Handler.Creator public interface IAuthenticationHandlerCreator { - Response CreateIdentityServerAuthenticationHandler(IApplicationBuilder app, AuthenticationOptions authOptions); + Response Create(IApplicationBuilder app, AuthenticationOptions authOptions); } } diff --git a/src/Ocelot/Authentication/Handler/Factory/AuthenticationHandlerFactory.cs b/src/Ocelot/Authentication/Handler/Factory/AuthenticationHandlerFactory.cs index 6379cc1f..60253816 100644 --- a/src/Ocelot/Authentication/Handler/Factory/AuthenticationHandlerFactory.cs +++ b/src/Ocelot/Authentication/Handler/Factory/AuthenticationHandlerFactory.cs @@ -19,7 +19,7 @@ namespace Ocelot.Authentication.Handler.Factory public Response Get(IApplicationBuilder app, AuthenticationOptions authOptions) { - var handler = _creator.CreateIdentityServerAuthenticationHandler(app, authOptions); + var handler = _creator.Create(app, authOptions); if (!handler.IsError) { diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs index ad30e166..037deeee 100644 --- a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs @@ -2,7 +2,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.Authentication.Handler.Factory; using Ocelot.Configuration; using Ocelot.Errors; diff --git a/src/Ocelot/Authorisation/ClaimsAuthoriser.cs b/src/Ocelot/Authorisation/ClaimsAuthoriser.cs index cb7849e9..96f7acd6 100644 --- a/src/Ocelot/Authorisation/ClaimsAuthoriser.cs +++ b/src/Ocelot/Authorisation/ClaimsAuthoriser.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using System.Security.Claims; using Ocelot.Errors; using Ocelot.Responses; diff --git a/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs b/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs index 547fc7f7..a86643a4 100644 --- a/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs +++ b/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Logging; -using Ocelot.Infrastructure.RequestData; +using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Responses; @@ -61,7 +60,7 @@ namespace Ocelot.Authorisation.Middleware SetPipelineError(new List { new UnauthorisedError( - $"{context.User.Identity.Name} unable to access {DownstreamRoute.ReRoute.UpstreamTemplate}") + $"{context.User.Identity.Name} unable to access {DownstreamRoute.ReRoute.UpstreamPathTemplate.Value}") }); } } diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs index 77024e70..948a7397 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -2,7 +2,6 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; diff --git a/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs b/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs index 1f6486f6..1b1745e2 100644 --- a/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs +++ b/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; diff --git a/src/Ocelot/Configuration/Authentication/HashMatcher.cs b/src/Ocelot/Configuration/Authentication/HashMatcher.cs new file mode 100644 index 00000000..08773332 --- /dev/null +++ b/src/Ocelot/Configuration/Authentication/HashMatcher.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; + +namespace Ocelot.Configuration.Authentication +{ + public class HashMatcher : IHashMatcher + { + public bool Match(string password, string salt, string hash) + { + byte[] s = Convert.FromBase64String(salt); + + string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2( + password: password, + salt: s, + prf: KeyDerivationPrf.HMACSHA256, + iterationCount: 10000, + numBytesRequested: 256 / 8)); + + return hashed == hash; + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Authentication/IHashMatcher.cs b/src/Ocelot/Configuration/Authentication/IHashMatcher.cs new file mode 100644 index 00000000..ad0d8e03 --- /dev/null +++ b/src/Ocelot/Configuration/Authentication/IHashMatcher.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Configuration.Authentication +{ + public interface IHashMatcher + { + bool Match(string password, string salt, string hash); + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Authentication/OcelotResourceOwnerPasswordValidator.cs b/src/Ocelot/Configuration/Authentication/OcelotResourceOwnerPasswordValidator.cs new file mode 100644 index 00000000..416c8ec2 --- /dev/null +++ b/src/Ocelot/Configuration/Authentication/OcelotResourceOwnerPasswordValidator.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using IdentityServer4.Models; +using IdentityServer4.Validation; +using Ocelot.Configuration.Provider; + +namespace Ocelot.Configuration.Authentication +{ + public class OcelotResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator + { + private readonly IHashMatcher _matcher; + private readonly IIdentityServerConfiguration _identityServerConfiguration; + + public OcelotResourceOwnerPasswordValidator(IHashMatcher matcher, IIdentityServerConfiguration identityServerConfiguration) + { + _identityServerConfiguration = identityServerConfiguration; + _matcher = matcher; + } + + public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) + { + try + { + var user = _identityServerConfiguration.Users.FirstOrDefault(u => u.UserName == context.UserName); + + if(user == null) + { + context.Result = new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + "invalid custom credential"); + } + else if(_matcher.Match(context.Password, user.Salt, user.Hash)) + { + context.Result = new GrantValidationResult( + subject: "admin", + authenticationMethod: "custom"); + } + else + { + context.Result = new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + "invalid custom credential"); + } + } + catch(Exception ex) + { + Console.WriteLine(ex); + } + + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs new file mode 100644 index 00000000..61d5e847 --- /dev/null +++ b/src/Ocelot/Configuration/Builder/AuthenticationOptionsBuilder.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration.Builder +{ + public class AuthenticationOptionsBuilder + { + + private string _provider; + private string _providerRootUrl; + private string _scopeName; + private string _scopeSecret; + private bool _requireHttps; + private List _additionalScopes; + + public AuthenticationOptionsBuilder WithProvider(string provider) + { + _provider = provider; + return this; + } + + public AuthenticationOptionsBuilder WithProviderRootUrl(string providerRootUrl) + { + _providerRootUrl = providerRootUrl; + return this; + } + + public AuthenticationOptionsBuilder WithScopeName(string scopeName) + { + _scopeName = scopeName; + return this; + } + + public AuthenticationOptionsBuilder WithScopeSecret(string scopeSecret) + { + _scopeSecret = scopeSecret; + return this; + } + + public AuthenticationOptionsBuilder WithRequireHttps(bool requireHttps) + { + _requireHttps = requireHttps; + return this; + } + + public AuthenticationOptionsBuilder WithAdditionalScopes(List additionalScopes) + { + _additionalScopes = additionalScopes; + return this; + } + + public AuthenticationOptions Build() + { + return new AuthenticationOptions(_provider, _providerRootUrl, _scopeName, _requireHttps, _additionalScopes, _scopeSecret); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs new file mode 100644 index 00000000..8d953d5c --- /dev/null +++ b/src/Ocelot/Configuration/Builder/QoSOptionsBuilder.cs @@ -0,0 +1,34 @@ +namespace Ocelot.Configuration.Builder +{ + public class QoSOptionsBuilder + { + private int _exceptionsAllowedBeforeBreaking; + + private int _durationOfBreak; + + private int _timeoutValue; + + public QoSOptionsBuilder WithExceptionsAllowedBeforeBreaking(int exceptionsAllowedBeforeBreaking) + { + _exceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; + return this; + } + + public QoSOptionsBuilder WithDurationOfBreak(int durationOfBreak) + { + _durationOfBreak = durationOfBreak; + return this; + } + + public QoSOptionsBuilder WithTimeoutValue(int timeoutValue) + { + _timeoutValue = timeoutValue; + return this; + } + + public QoSOptions Build() + { + return new QoSOptions(_exceptionsAllowedBeforeBreaking, _durationOfBreak, _timeoutValue); + } + } +} diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index f77ae66b..1fc31f03 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -1,20 +1,18 @@ using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Values; namespace Ocelot.Configuration.Builder { public class ReRouteBuilder { - private string _downstreamTemplate; + private AuthenticationOptions _authenticationOptions; + private string _loadBalancerKey; + private string _downstreamPathTemplate; private string _upstreamTemplate; private string _upstreamTemplatePattern; private string _upstreamHttpMethod; private bool _isAuthenticated; - private string _authenticationProvider; - private string _authenticationProviderUrl; - private string _scopeName; - private List _additionalScopes; - private bool _requireHttps; - private string _scopeSecret; private List _configHeaderExtractorProperties; private List _claimToClaims; private Dictionary _routeClaimRequirement; @@ -23,19 +21,41 @@ namespace Ocelot.Configuration.Builder private string _requestIdHeaderKey; private bool _isCached; private CacheOptions _fileCacheOptions; + private string _downstreamScheme; + private string _downstreamHost; + private int _downstreamPort; + private string _loadBalancer; + private ServiceProviderConfiguraion _serviceProviderConfiguraion; + private bool _useQos; + private QoSOptions _qosOptions; + public bool _enableRateLimiting; + public RateLimitOptions _rateLimitOptions; - public ReRouteBuilder() + public ReRouteBuilder WithLoadBalancer(string loadBalancer) { - _additionalScopes = new List(); - } - - public ReRouteBuilder WithDownstreamTemplate(string input) - { - _downstreamTemplate = input; + _loadBalancer = loadBalancer; return this; } - public ReRouteBuilder WithUpstreamTemplate(string input) + public ReRouteBuilder WithDownstreamScheme(string downstreamScheme) + { + _downstreamScheme = downstreamScheme; + return this; + } + + public ReRouteBuilder WithDownstreamHost(string downstreamHost) + { + _downstreamHost = downstreamHost; + return this; + } + + public ReRouteBuilder WithDownstreamPathTemplate(string input) + { + _downstreamPathTemplate = input; + return this; + } + + public ReRouteBuilder WithUpstreamPathTemplate(string input) { _upstreamTemplate = input; return this; @@ -63,42 +83,6 @@ namespace Ocelot.Configuration.Builder return this; } - public ReRouteBuilder WithAuthenticationProvider(string input) - { - _authenticationProvider = input; - return this; - } - - public ReRouteBuilder WithAuthenticationProviderUrl(string input) - { - _authenticationProviderUrl = input; - return this; - } - - public ReRouteBuilder WithAuthenticationProviderScopeName(string input) - { - _scopeName = input; - return this; - } - - public ReRouteBuilder WithAuthenticationProviderAdditionalScopes(List input) - { - _additionalScopes = input; - return this; - } - - public ReRouteBuilder WithRequireHttps(bool input) - { - _requireHttps = input; - return this; - } - - public ReRouteBuilder WithScopeSecret(string input) - { - _scopeSecret = input; - return this; - } - public ReRouteBuilder WithRequestIdKey(string input) { _requestIdHeaderKey = input; @@ -141,12 +125,83 @@ namespace Ocelot.Configuration.Builder return this; } + public ReRouteBuilder WithDownstreamPort(int port) + { + _downstreamPort = port; + return this; + } + + public ReRouteBuilder WithIsQos(bool input) + { + _useQos = input; + return this; + } + + public ReRouteBuilder WithQosOptions(QoSOptions input) + { + _qosOptions = input; + return this; + } + + + public ReRouteBuilder WithLoadBalancerKey(string loadBalancerKey) + { + _loadBalancerKey = loadBalancerKey; + return this; + } + + public ReRouteBuilder WithServiceProviderConfiguraion(ServiceProviderConfiguraion serviceProviderConfiguraion) + { + _serviceProviderConfiguraion = serviceProviderConfiguraion; + return this; + } + + public ReRouteBuilder WithAuthenticationOptions(AuthenticationOptions authenticationOptions) + { + _authenticationOptions = authenticationOptions; + return this; + } + + public ReRouteBuilder WithEnableRateLimiting(bool input) + { + _enableRateLimiting = input; + return this; + } + + public ReRouteBuilder WithRateLimitOptions(RateLimitOptions input) + { + _rateLimitOptions = input; + return this; + } + + public ReRoute Build() { - return new ReRoute(_downstreamTemplate, _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, - _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, - _requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement, - _isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions); + return new ReRoute( + new PathTemplate(_downstreamPathTemplate), + new PathTemplate(_upstreamTemplate), + new HttpMethod(_upstreamHttpMethod), + _upstreamTemplatePattern, + _isAuthenticated, + _authenticationOptions, + _configHeaderExtractorProperties, + _claimToClaims, + _routeClaimRequirement, + _isAuthorised, + _claimToQueries, + _requestIdHeaderKey, + _isCached, + _fileCacheOptions, + _downstreamScheme, + _loadBalancer, + _downstreamHost, + _downstreamPort, + _loadBalancerKey, + _serviceProviderConfiguraion, + _useQos, + _qosOptions, + _enableRateLimiting, + _rateLimitOptions); } } } diff --git a/src/Ocelot/Configuration/Builder/ServiceProviderConfiguraionBuilder.cs b/src/Ocelot/Configuration/Builder/ServiceProviderConfiguraionBuilder.cs new file mode 100644 index 00000000..129acae8 --- /dev/null +++ b/src/Ocelot/Configuration/Builder/ServiceProviderConfiguraionBuilder.cs @@ -0,0 +1,62 @@ +namespace Ocelot.Configuration.Builder +{ + public class ServiceProviderConfiguraionBuilder + { + private string _serviceName; + private string _downstreamHost; + private int _downstreamPort; + private bool _userServiceDiscovery; + private string _serviceDiscoveryProvider; + private string _serviceDiscoveryProviderHost; + private int _serviceDiscoveryProviderPort; + + public ServiceProviderConfiguraionBuilder WithServiceName(string serviceName) + { + _serviceName = serviceName; + return this; + } + + public ServiceProviderConfiguraionBuilder WithDownstreamHost(string downstreamHost) + { + _downstreamHost = downstreamHost; + return this; + } + + public ServiceProviderConfiguraionBuilder WithDownstreamPort(int downstreamPort) + { + _downstreamPort = downstreamPort; + return this; + } + + public ServiceProviderConfiguraionBuilder WithUseServiceDiscovery(bool userServiceDiscovery) + { + _userServiceDiscovery = userServiceDiscovery; + return this; + } + + public ServiceProviderConfiguraionBuilder WithServiceDiscoveryProvider(string serviceDiscoveryProvider) + { + _serviceDiscoveryProvider = serviceDiscoveryProvider; + return this; + } + + public ServiceProviderConfiguraionBuilder WithServiceDiscoveryProviderHost(string serviceDiscoveryProviderHost) + { + _serviceDiscoveryProviderHost = serviceDiscoveryProviderHost; + return this; + } + + public ServiceProviderConfiguraionBuilder WithServiceDiscoveryProviderPort(int serviceDiscoveryProviderPort) + { + _serviceDiscoveryProviderPort = serviceDiscoveryProviderPort; + return this; + } + + + public ServiceProviderConfiguraion Build() + { + return new ServiceProviderConfiguraion(_serviceName, _downstreamHost, _downstreamPort, _userServiceDiscovery, + _serviceDiscoveryProvider, _serviceDiscoveryProviderHost,_serviceDiscoveryProviderPort); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs index f74f880b..b4abdec8 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -1,11 +1,15 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Validator; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Requester.QoS; using Ocelot.Responses; using Ocelot.Utilities; @@ -21,36 +25,52 @@ namespace Ocelot.Configuration.Creator private const string RegExMatchEverything = ".*"; private const string RegExMatchEndString = "$"; private const string RegExIgnoreCase = "(?i)"; + private const string RegExForwardSlashOnly = "^/$"; private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; private readonly ILogger _logger; + private readonly ILoadBalancerFactory _loadBalanceFactory; + private readonly ILoadBalancerHouse _loadBalancerHouse; + private readonly IQoSProviderFactory _qoSProviderFactory; + private readonly IQosProviderHouse _qosProviderHouse; public FileOcelotConfigurationCreator( IOptions options, IConfigurationValidator configurationValidator, IClaimToThingConfigurationParser claimToThingConfigurationParser, - ILogger logger) + ILogger logger, + ILoadBalancerFactory loadBalancerFactory, + ILoadBalancerHouse loadBalancerHouse, + IQoSProviderFactory qoSProviderFactory, + IQosProviderHouse qosProviderHouse) { + _loadBalanceFactory = loadBalancerFactory; + _loadBalancerHouse = loadBalancerHouse; + _qoSProviderFactory = qoSProviderFactory; + _qosProviderHouse = qosProviderHouse; _options = options; _configurationValidator = configurationValidator; _claimToThingConfigurationParser = claimToThingConfigurationParser; _logger = logger; } - public Response Create() + public async Task> Create() { - var config = SetUpConfiguration(); + var config = await SetUpConfiguration(_options.Value); return new OkResponse(config); } - /// - /// This method is meant to be tempoary to convert a config to an ocelot config...probably wont keep this but we will see - /// will need a refactor at some point as its crap - /// - private IOcelotConfiguration SetUpConfiguration() + public async Task> Create(FileConfiguration fileConfiguration) + { + var config = await SetUpConfiguration(fileConfiguration); + + return new OkResponse(config); + } + + private async Task SetUpConfiguration(FileConfiguration fileConfiguration) { - var response = _configurationValidator.IsValid(_options.Value); + var response = _configurationValidator.IsValid(fileConfiguration); if (response.Data.IsError) { @@ -66,59 +86,190 @@ namespace Ocelot.Configuration.Creator var reRoutes = new List(); - foreach (var reRoute in _options.Value.ReRoutes) + foreach (var reRoute in fileConfiguration.ReRoutes) { - var ocelotReRoute = SetUpReRoute(reRoute, _options.Value.GlobalConfiguration); + var ocelotReRoute = await SetUpReRoute(reRoute, fileConfiguration.GlobalConfiguration); reRoutes.Add(ocelotReRoute); } - return new OcelotConfiguration(reRoutes); + return new OcelotConfiguration(reRoutes, fileConfiguration.GlobalConfiguration.AdministrationPath); } - private ReRoute SetUpReRoute(FileReRoute reRoute, FileGlobalConfiguration globalConfiguration) + private async Task SetUpReRoute(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) + { + var isAuthenticated = IsAuthenticated(fileReRoute); + + var isAuthorised = IsAuthorised(fileReRoute); + + var isCached = IsCached(fileReRoute); + + var requestIdKey = BuildRequestId(fileReRoute, globalConfiguration); + + var reRouteKey = BuildReRouteKey(fileReRoute); + + var upstreamTemplatePattern = BuildUpstreamTemplatePattern(fileReRoute); + + var isQos = IsQoS(fileReRoute); + + var serviceProviderConfiguration = BuildServiceProviderConfiguration(fileReRoute, globalConfiguration); + + var authOptionsForRoute = BuildAuthenticationOptions(fileReRoute); + + var claimsToHeaders = BuildAddThingsToRequest(fileReRoute.AddHeadersToRequest); + + var claimsToClaims = BuildAddThingsToRequest(fileReRoute.AddClaimsToRequest); + + var claimsToQueries = BuildAddThingsToRequest(fileReRoute.AddQueriesToRequest); + + var qosOptions = BuildQoSOptions(fileReRoute); + + var enableRateLimiting = IsEnableRateLimiting(fileReRoute); + + var rateLimitOption = BuildRateLimitOptions(fileReRoute, globalConfiguration, enableRateLimiting); + + var reRoute = new ReRouteBuilder() + .WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate) + .WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate) + .WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod) + .WithUpstreamTemplatePattern(upstreamTemplatePattern) + .WithIsAuthenticated(isAuthenticated) + .WithAuthenticationOptions(authOptionsForRoute) + .WithClaimsToHeaders(claimsToHeaders) + .WithClaimsToClaims(claimsToClaims) + .WithRouteClaimsRequirement(fileReRoute.RouteClaimsRequirement) + .WithIsAuthorised(isAuthorised) + .WithClaimsToQueries(claimsToQueries) + .WithRequestIdKey(requestIdKey) + .WithIsCached(isCached) + .WithCacheOptions(new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds)) + .WithDownstreamScheme(fileReRoute.DownstreamScheme) + .WithLoadBalancer(fileReRoute.LoadBalancer) + .WithDownstreamHost(fileReRoute.DownstreamHost) + .WithDownstreamPort(fileReRoute.DownstreamPort) + .WithLoadBalancerKey(reRouteKey) + .WithServiceProviderConfiguraion(serviceProviderConfiguration) + .WithIsQos(isQos) + .WithQosOptions(qosOptions) + .WithEnableRateLimiting(enableRateLimiting) + .WithRateLimitOptions(rateLimitOption) + .Build(); + await SetupLoadBalancer(reRoute); + SetupQosProvider(reRoute); + return reRoute; + } + + private static RateLimitOptions BuildRateLimitOptions(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration, bool enableRateLimiting) + { + RateLimitOptions rateLimitOption = null; + if (enableRateLimiting) + { + rateLimitOption = new RateLimitOptions(enableRateLimiting, globalConfiguration.RateLimitOptions.ClientIdHeader, + fileReRoute.RateLimitOptions.ClientWhitelist, globalConfiguration.RateLimitOptions.DisableRateLimitHeaders, + globalConfiguration.RateLimitOptions.QuotaExceededMessage, globalConfiguration.RateLimitOptions.RateLimitCounterPrefix, + new RateLimitRule(fileReRoute.RateLimitOptions.Period, TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan), fileReRoute.RateLimitOptions.Limit) + , globalConfiguration.RateLimitOptions.HttpStatusCode); + } + + return rateLimitOption; + } + + private static bool IsEnableRateLimiting(FileReRoute fileReRoute) + { + return (fileReRoute.RateLimitOptions != null && fileReRoute.RateLimitOptions.EnableRateLimiting) ? true : false; + } + + private QoSOptions BuildQoSOptions(FileReRoute fileReRoute) + { + return new QoSOptionsBuilder() + .WithExceptionsAllowedBeforeBreaking(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking) + .WithDurationOfBreak(fileReRoute.QoSOptions.DurationOfBreak) + .WithTimeoutValue(fileReRoute.QoSOptions.TimeoutValue) + .Build(); + } + + private bool IsQoS(FileReRoute fileReRoute) + { + return fileReRoute.QoSOptions?.ExceptionsAllowedBeforeBreaking > 0 && fileReRoute.QoSOptions?.TimeoutValue > 0; + } + + private bool IsAuthenticated(FileReRoute fileReRoute) + { + return !string.IsNullOrEmpty(fileReRoute.AuthenticationOptions?.Provider); + } + + private bool IsAuthorised(FileReRoute fileReRoute) + { + return fileReRoute.RouteClaimsRequirement?.Count > 0; + } + + private bool IsCached(FileReRoute fileReRoute) + { + return fileReRoute.FileCacheOptions.TtlSeconds > 0; + } + + private string BuildRequestId(FileReRoute fileReRoute, 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 + var requestIdKey = globalRequestIdConfiguration ? globalConfiguration.RequestIdKey - : reRoute.RequestIdKey; + : fileReRoute.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)); + return requestIdKey; } - private string BuildUpstreamTemplate(FileReRoute reRoute) + private string BuildReRouteKey(FileReRoute fileReRoute) { - var upstreamTemplate = reRoute.UpstreamTemplate; + //note - not sure if this is the correct key, but this is probably the only unique key i can think of given my poor brain + var loadBalancerKey = $"{fileReRoute.UpstreamPathTemplate}{fileReRoute.UpstreamHttpMethod}"; + return loadBalancerKey; + } + + private AuthenticationOptions BuildAuthenticationOptions(FileReRoute fileReRoute) + { + return new AuthenticationOptionsBuilder() + .WithProvider(fileReRoute.AuthenticationOptions?.Provider) + .WithProviderRootUrl(fileReRoute.AuthenticationOptions?.ProviderRootUrl) + .WithScopeName(fileReRoute.AuthenticationOptions?.ScopeName) + .WithRequireHttps(fileReRoute.AuthenticationOptions.RequireHttps) + .WithAdditionalScopes(fileReRoute.AuthenticationOptions?.AdditionalScopes) + .WithScopeSecret(fileReRoute.AuthenticationOptions?.ScopeSecret) + .Build(); + } + + private async Task SetupLoadBalancer(ReRoute reRoute) + { + var loadBalancer = await _loadBalanceFactory.Get(reRoute); + _loadBalancerHouse.Add(reRoute.ReRouteKey, loadBalancer); + } + + private void SetupQosProvider(ReRoute reRoute) + { + var loadBalancer = _qoSProviderFactory.Get(reRoute); + _qosProviderHouse.Add(reRoute.ReRouteKey, loadBalancer); + } + + private ServiceProviderConfiguraion BuildServiceProviderConfiguration(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration) + { + var useServiceDiscovery = !string.IsNullOrEmpty(fileReRoute.ServiceName) + && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider); + + var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; + + return new ServiceProviderConfiguraionBuilder() + .WithServiceName(fileReRoute.ServiceName) + .WithDownstreamHost(fileReRoute.DownstreamHost) + .WithDownstreamPort(fileReRoute.DownstreamPort) + .WithUseServiceDiscovery(useServiceDiscovery) + .WithServiceDiscoveryProvider(globalConfiguration?.ServiceDiscoveryProvider?.Provider) + .WithServiceDiscoveryProviderHost(globalConfiguration?.ServiceDiscoveryProvider?.Host) + .WithServiceDiscoveryProviderPort(serviceProviderPort) + .Build(); + } + + private string BuildUpstreamTemplatePattern(FileReRoute reRoute) + { + var upstreamTemplate = reRoute.UpstreamPathTemplate; upstreamTemplate = upstreamTemplate.SetLastCharacterAs('/'); @@ -140,6 +291,11 @@ namespace Ocelot.Configuration.Creator upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything); } + if (upstreamTemplate == "/") + { + return RegExForwardSlashOnly; + } + var route = reRoute.ReRouteIsCaseSensitive ? $"{upstreamTemplate}{RegExMatchEndString}" : $"{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; @@ -147,7 +303,7 @@ namespace Ocelot.Configuration.Creator return route; } - private List GetAddThingsToRequest(Dictionary thingBeingAdded) + private List BuildAddThingsToRequest(Dictionary thingBeingAdded) { var claimsToTHings = new List(); diff --git a/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs index 6cc7c2e8..99d1d39d 100644 --- a/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs @@ -1,9 +1,12 @@ +using System.Threading.Tasks; +using Ocelot.Configuration.File; using Ocelot.Responses; namespace Ocelot.Configuration.Creator { public interface IOcelotConfigurationCreator { - Response Create(); + Task> Create(); + Task> Create(FileConfiguration fileConfiguration); } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs new file mode 100644 index 00000000..48819608 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; +using Ocelot.Configuration.Provider; + +namespace Ocelot.Configuration.Creator +{ + public static class IdentityServerConfigurationCreator + { + public static IdentityServerConfiguration GetIdentityServerConfiguration() + { + var username = Environment.GetEnvironmentVariable("OCELOT_USERNAME"); + var hash = Environment.GetEnvironmentVariable("OCELOT_HASH"); + var salt = Environment.GetEnvironmentVariable("OCELOT_SALT"); + + return new IdentityServerConfiguration( + "admin", + false, + SupportedTokens.Both, + "secret", + new List { "admin", "openid", "offline_access" }, + "Ocelot Administration", + true, + GrantTypes.ResourceOwnerPassword, + AccessTokenType.Jwt, + false, + new List + { + new User("admin", username, hash, salt) + } + ); + } + } +} diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 07c17188..4d34f6de 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -1,7 +1,19 @@ -namespace Ocelot.Configuration.File + +namespace Ocelot.Configuration.File { public class FileGlobalConfiguration { + public FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider(); + RateLimitOptions = new FileRateLimitOptions(); + } + public string RequestIdKey { get; set; } + + public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;} + public string AdministrationPath {get;set;} + + public FileRateLimitOptions RateLimitOptions { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs.orig b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs.orig new file mode 100644 index 00000000..8162485f --- /dev/null +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs.orig @@ -0,0 +1,22 @@ + +namespace Ocelot.Configuration.File +{ + public class FileGlobalConfiguration + { + public FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider(); + RateLimitOptions = new FileRateLimitOptions(); + } + + public string RequestIdKey { get; set; } + + public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;} +<<<<<<< HEAD + public string AdministrationPath {get;set;} +======= + + public FileRateLimitOptions RateLimitOptions { get; set; } +>>>>>>> develop + } +} diff --git a/src/Ocelot/Configuration/File/FileQoSOptions.cs b/src/Ocelot/Configuration/File/FileQoSOptions.cs new file mode 100644 index 00000000..876a3fd5 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileQoSOptions.cs @@ -0,0 +1,11 @@ +namespace Ocelot.Configuration.File +{ + public class FileQoSOptions + { + public int ExceptionsAllowedBeforeBreaking { get; set; } + + public int DurationOfBreak { get; set; } + + public int TimeoutValue { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileRateLimitOptions.cs b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs new file mode 100644 index 00000000..afb3fab5 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileRateLimitOptions.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.Configuration.File +{ + public class FileRateLimitOptions + { + /// + /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId + /// + public string ClientIdHeader { get; set; } = "ClientId"; + + /// + /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message. + /// If none specified the default will be: + /// API calls quota exceeded! maximum admitted {0} per {1} + /// + public string QuotaExceededMessage { get; set; } + + /// + /// Gets or sets the counter prefix, used to compose the rate limit counter cache key + /// + public string RateLimitCounterPrefix { get; set; } = "ocelot"; + + /// + /// Disables X-Rate-Limit and Rety-After headers + /// + public bool DisableRateLimitHeaders { get; set; } + + /// + /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests) + /// + public int HttpStatusCode { get; set; } = 429; + } + + +} diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs new file mode 100644 index 00000000..727a9e82 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.Configuration.File +{ + + public class FileRateLimitRule + { + public FileRateLimitRule() + { + ClientWhitelist = new List(); + } + + public List ClientWhitelist { get; set; } + + /// + /// Enables endpoint rate limiting based URL path and HTTP verb + /// + public bool EnableRateLimiting { get; set; } + + /// + /// Rate limit period as in 1s, 1m, 1h + /// + public string Period { get; set; } + + public double PeriodTimespan { get; set; } + /// + /// Maximum number of requests that a client can make in a defined period + /// + public long Limit { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index 3773dd9d..07562653 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -12,10 +12,12 @@ namespace Ocelot.Configuration.File AddQueriesToRequest = new Dictionary(); AuthenticationOptions = new FileAuthenticationOptions(); FileCacheOptions = new FileCacheOptions(); + QoSOptions = new FileQoSOptions(); + RateLimitOptions = new FileRateLimitRule(); } - public string DownstreamTemplate { get; set; } - public string UpstreamTemplate { get; set; } + public string DownstreamPathTemplate { get; set; } + public string UpstreamPathTemplate { get; set; } public string UpstreamHttpMethod { get; set; } public FileAuthenticationOptions AuthenticationOptions { get; set; } public Dictionary AddHeadersToRequest { get; set; } @@ -25,5 +27,12 @@ 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;} + public int DownstreamPort { get; set; } + public FileQoSOptions QoSOptions { get; set; } + public string LoadBalancer {get;set;} + public FileRateLimitRule RateLimitOptions { 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..70a3c42f --- /dev/null +++ b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs @@ -0,0 +1,10 @@ +namespace Ocelot.Configuration.File +{ + public class FileServiceDiscoveryProvider + { + + public string Provider {get;set;} + public string Host {get;set;} + public int Port { get; set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/IOcelotConfiguration.cs b/src/Ocelot/Configuration/IOcelotConfiguration.cs index 8359a2e1..566e2f91 100644 --- a/src/Ocelot/Configuration/IOcelotConfiguration.cs +++ b/src/Ocelot/Configuration/IOcelotConfiguration.cs @@ -5,5 +5,6 @@ namespace Ocelot.Configuration public interface IOcelotConfiguration { List ReRoutes { get; } + string AdministrationPath {get;} } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/OcelotConfiguration.cs b/src/Ocelot/Configuration/OcelotConfiguration.cs index 3b0858eb..5655b3c2 100644 --- a/src/Ocelot/Configuration/OcelotConfiguration.cs +++ b/src/Ocelot/Configuration/OcelotConfiguration.cs @@ -4,11 +4,13 @@ namespace Ocelot.Configuration { public class OcelotConfiguration : IOcelotConfiguration { - public OcelotConfiguration(List reRoutes) + public OcelotConfiguration(List reRoutes, string administrationPath) { ReRoutes = reRoutes; + AdministrationPath = administrationPath; } public List ReRoutes { get; } + public string AdministrationPath {get;} } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/FileConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/FileConfigurationProvider.cs new file mode 100644 index 00000000..5f6dbe08 --- /dev/null +++ b/src/Ocelot/Configuration/Provider/FileConfigurationProvider.cs @@ -0,0 +1,25 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Provider +{ + public class FileConfigurationProvider : IFileConfigurationProvider + { + private IFileConfigurationRepository _repo; + + public FileConfigurationProvider(IFileConfigurationRepository repo) + { + _repo = repo; + } + + public Response Get() + { + var fileConfig = _repo.Get(); + return new OkResponse(fileConfig.Data); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/IFileConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/IFileConfigurationProvider.cs new file mode 100644 index 00000000..3a4dca14 --- /dev/null +++ b/src/Ocelot/Configuration/Provider/IFileConfigurationProvider.cs @@ -0,0 +1,10 @@ +using Ocelot.Configuration.File; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Provider +{ + public interface IFileConfigurationProvider + { + Response Get(); + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs b/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs new file mode 100644 index 00000000..bb66265f --- /dev/null +++ b/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; + +namespace Ocelot.Configuration.Provider +{ + public interface IIdentityServerConfiguration + { + string ApiName { get; } + bool RequireHttps { get; } + List AllowedScopes { get; } + SupportedTokens SupportedTokens { get; } + string ApiSecret { get; } + string Description {get;} + bool Enabled {get;} + IEnumerable AllowedGrantTypes {get;} + AccessTokenType AccessTokenType {get;} + bool RequireClientSecret {get;} + List Users {get;} + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs b/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs new file mode 100644 index 00000000..f0f6897d --- /dev/null +++ b/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using IdentityServer4.AccessTokenValidation; +using IdentityServer4.Models; + +namespace Ocelot.Configuration.Provider +{ + public class IdentityServerConfiguration : IIdentityServerConfiguration + { + public IdentityServerConfiguration( + string apiName, + bool requireHttps, + SupportedTokens supportedTokens, + string apiSecret, + List allowedScopes, + string description, + bool enabled, + IEnumerable grantType, + AccessTokenType accessTokenType, + bool requireClientSecret, + List users) + { + ApiName = apiName; + RequireHttps = requireHttps; + SupportedTokens = supportedTokens; + ApiSecret = apiSecret; + AllowedScopes = allowedScopes; + Description = description; + Enabled = enabled; + AllowedGrantTypes = grantType; + AccessTokenType = accessTokenType; + RequireClientSecret = requireClientSecret; + Users = users; + } + + public string ApiName { get; private set; } + public bool RequireHttps { get; private set; } + public List AllowedScopes { get; private set; } + public SupportedTokens SupportedTokens { get; private set; } + public string ApiSecret { get; private set; } + public string Description {get;private set;} + public bool Enabled {get;private set;} + public IEnumerable AllowedGrantTypes {get;private set;} + public AccessTokenType AccessTokenType {get;private set;} + public bool RequireClientSecret {get;private set;} + public List Users {get;private set;} + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs index 4b6c5fd2..e416ed8e 100644 --- a/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs +++ b/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs @@ -1,5 +1,4 @@ -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.Repository; +using Ocelot.Configuration.Repository; using Ocelot.Responses; namespace Ocelot.Configuration.Provider @@ -10,13 +9,10 @@ namespace Ocelot.Configuration.Provider public class OcelotConfigurationProvider : IOcelotConfigurationProvider { private readonly IOcelotConfigurationRepository _repo; - private readonly IOcelotConfigurationCreator _creator; - public OcelotConfigurationProvider(IOcelotConfigurationRepository repo, - IOcelotConfigurationCreator creator) + public OcelotConfigurationProvider(IOcelotConfigurationRepository repo) { _repo = repo; - _creator = creator; } public Response Get() @@ -28,20 +24,6 @@ namespace Ocelot.Configuration.Provider return new ErrorResponse(repoConfig.Errors); } - if (repoConfig.Data == null) - { - var creatorConfig = _creator.Create(); - - if (creatorConfig.IsError) - { - return new ErrorResponse(creatorConfig.Errors); - } - - _repo.AddOrReplace(creatorConfig.Data); - - return new OkResponse(creatorConfig.Data); - } - return new OkResponse(repoConfig.Data); } } diff --git a/src/Ocelot/Configuration/Provider/User.cs b/src/Ocelot/Configuration/Provider/User.cs new file mode 100644 index 00000000..f61ff4e5 --- /dev/null +++ b/src/Ocelot/Configuration/Provider/User.cs @@ -0,0 +1,17 @@ +namespace Ocelot.Configuration.Provider +{ + public class User + { + public User(string subject, string userName, string hash, string salt) + { + Subject = subject; + UserName = userName; + Hash = hash; + Salt = salt; + } + public string Subject { get; private set; } + public string UserName { get; private set; } + public string Hash { get; private set; } + public string Salt { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs new file mode 100644 index 00000000..29145888 --- /dev/null +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -0,0 +1,30 @@ +using System; +using Polly.Timeout; + +namespace Ocelot.Configuration +{ + public class QoSOptions + { + public QoSOptions( + int exceptionsAllowedBeforeBreaking, + int durationofBreak, + int timeoutValue, + TimeoutStrategy timeoutStrategy = TimeoutStrategy.Pessimistic) + { + ExceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; + DurationOfBreak = TimeSpan.FromMilliseconds(durationofBreak); + TimeoutValue = TimeSpan.FromMilliseconds(timeoutValue); + TimeoutStrategy = timeoutStrategy; + } + + + public int ExceptionsAllowedBeforeBreaking { get; private set; } + + public TimeSpan DurationOfBreak { get; private set; } + + public TimeSpan TimeoutValue { get; private set; } + + public TimeoutStrategy TimeoutStrategy { get; private set; } + + } +} diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs new file mode 100644 index 00000000..85a3bf78 --- /dev/null +++ b/src/Ocelot/Configuration/RateLimitOptions.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.Configuration +{ + /// + /// RateLimit Options + /// + public class RateLimitOptions + { + public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List clientWhitelist,bool disableRateLimitHeaders, + string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode) + { + EnableRateLimiting = enbleRateLimiting; + ClientIdHeader = clientIdHeader; + ClientWhitelist = clientWhitelist?? new List(); + DisableRateLimitHeaders = disableRateLimitHeaders; + QuotaExceededMessage = quotaExceededMessage; + RateLimitCounterPrefix = rateLimitCounterPrefix; + RateLimitRule = rateLimitRule; + HttpStatusCode = httpStatusCode; + } + + public RateLimitRule RateLimitRule { get; private set; } + + public List ClientWhitelist { get; private set; } + + /// + /// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId + /// + public string ClientIdHeader { get; private set; } + + /// + /// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests) + /// + public int HttpStatusCode { get; private set; } + + /// + /// Gets or sets a value that will be used as a formatter for the QuotaExceeded response message. + /// If none specified the default will be: + /// API calls quota exceeded! maximum admitted {0} per {1} + /// + public string QuotaExceededMessage { get; private set; } + + /// + /// Gets or sets the counter prefix, used to compose the rate limit counter cache key + /// + public string RateLimitCounterPrefix { get; private set; } + + /// + /// Enables endpoint rate limiting based URL path and HTTP verb + /// + public bool EnableRateLimiting { get; private set; } + + /// + /// Disables X-Rate-Limit and Rety-After headers + /// + public bool DisableRateLimitHeaders { get; private set; } + } + + public class RateLimitRule + { + public RateLimitRule(string period, TimeSpan periodTimespan, long limit) + { + Period = period; + PeriodTimespan = periodTimespan; + Limit = limit; + } + + /// + /// Rate limit period as in 1s, 1m, 1h,1d + /// + public string Period { get; private set; } + + public TimeSpan PeriodTimespan { get; private set; } + /// + /// Maximum number of requests that a client can make in a defined period + /// + public long Limit { get; private set; } + } +} diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index 433250b3..f629867a 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -1,16 +1,43 @@ using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Values; namespace Ocelot.Configuration { public class ReRoute { - 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) + public ReRoute(PathTemplate downstreamPathTemplate, + PathTemplate upstreamTemplate, + HttpMethod upstreamHttpMethod, + string upstreamTemplatePattern, + bool isAuthenticated, + AuthenticationOptions authenticationOptions, + List configurationHeaderExtractorProperties, + List claimsToClaims, + Dictionary routeClaimsRequirement, + bool isAuthorised, + List claimsToQueries, + string requestIdKey, + bool isCached, + CacheOptions fileCacheOptions, + string downstreamScheme, + string loadBalancer, + string downstreamHost, + int downstreamPort, + string reRouteKey, + ServiceProviderConfiguraion serviceProviderConfiguraion, + bool isQos, + QoSOptions qos, + bool enableRateLimit, + RateLimitOptions ratelimitOptions) { - DownstreamTemplate = downstreamTemplate; - UpstreamTemplate = upstreamTemplate; + ReRouteKey = reRouteKey; + ServiceProviderConfiguraion = serviceProviderConfiguraion; + LoadBalancer = loadBalancer; + DownstreamHost = downstreamHost; + DownstreamPort = downstreamPort; + DownstreamPathTemplate = downstreamPathTemplate; + UpstreamPathTemplate = upstreamTemplate; UpstreamHttpMethod = upstreamHttpMethod; UpstreamTemplatePattern = upstreamTemplatePattern; IsAuthenticated = isAuthenticated; @@ -26,12 +53,18 @@ namespace Ocelot.Configuration ?? new List(); ClaimsToHeaders = configurationHeaderExtractorProperties ?? new List(); + DownstreamScheme = downstreamScheme; + IsQos = isQos; + QosOptions = qos; + EnableEndpointRateLimiting = enableRateLimit; + RateLimitOptions = ratelimitOptions; } - public string DownstreamTemplate { get; private set; } - public string UpstreamTemplate { get; private set; } + public string ReRouteKey {get;private set;} + public PathTemplate DownstreamPathTemplate { get; private set; } + public PathTemplate UpstreamPathTemplate { get; private set; } public string UpstreamTemplatePattern { get; private set; } - public string UpstreamHttpMethod { get; private set; } + public HttpMethod UpstreamHttpMethod { get; private set; } public bool IsAuthenticated { get; private set; } public bool IsAuthorised { get; private set; } public AuthenticationOptions AuthenticationOptions { get; private set; } @@ -42,5 +75,14 @@ namespace Ocelot.Configuration public string RequestIdKey { get; private set; } public bool IsCached { get; private set; } public CacheOptions FileCacheOptions { get; private set; } + public string DownstreamScheme {get;private set;} + public bool IsQos { get; private set; } + public QoSOptions QosOptions { get; private set; } + public string LoadBalancer {get;private set;} + public string DownstreamHost { get; private set; } + public int DownstreamPort { get; private set; } + public ServiceProviderConfiguraion ServiceProviderConfiguraion { get; private set; } + public bool EnableEndpointRateLimiting { get; private set; } + public RateLimitOptions RateLimitOptions { get; private set; } } } \ No newline at end of file diff --git a/src/Ocelot/Configuration/Repository/FileConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/FileConfigurationRepository.cs new file mode 100644 index 00000000..f32dcaec --- /dev/null +++ b/src/Ocelot/Configuration/Repository/FileConfigurationRepository.cs @@ -0,0 +1,42 @@ +using System; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Repository +{ + public class FileConfigurationRepository : IFileConfigurationRepository + { + private static readonly object _lock = new object(); + public Response Get() + { + var configFilePath = $"{AppContext.BaseDirectory}/configuration.json"; + string json = string.Empty; + lock(_lock) + { + json = System.IO.File.ReadAllText(configFilePath); + } + var fileConfiguration = JsonConvert.DeserializeObject(json); + return new OkResponse(fileConfiguration); + } + + public Response Set(FileConfiguration fileConfiguration) + { + var configurationPath = $"{AppContext.BaseDirectory}/configuration.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + lock(_lock) + { + if (System.IO.File.Exists(configurationPath)) + { + System.IO.File.Delete(configurationPath); + } + + System.IO.File.WriteAllText(configurationPath, jsonConfiguration); + } + + return new OkResponse(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Repository/IFileConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/IFileConfigurationRepository.cs new file mode 100644 index 00000000..26726b46 --- /dev/null +++ b/src/Ocelot/Configuration/Repository/IFileConfigurationRepository.cs @@ -0,0 +1,11 @@ +using Ocelot.Configuration.File; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Repository +{ + public interface IFileConfigurationRepository + { + Response Get(); + Response Set(FileConfiguration fileConfiguration); + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/ServiceProviderConfiguraion.cs b/src/Ocelot/Configuration/ServiceProviderConfiguraion.cs new file mode 100644 index 00000000..d471a9e5 --- /dev/null +++ b/src/Ocelot/Configuration/ServiceProviderConfiguraion.cs @@ -0,0 +1,25 @@ +namespace Ocelot.Configuration +{ + public class ServiceProviderConfiguraion + { + public ServiceProviderConfiguraion(string serviceName, string downstreamHost, + int downstreamPort, bool useServiceDiscovery, string serviceDiscoveryProvider, string serviceProviderHost, int serviceProviderPort) + { + ServiceName = serviceName; + DownstreamHost = downstreamHost; + DownstreamPort = downstreamPort; + UseServiceDiscovery = useServiceDiscovery; + ServiceDiscoveryProvider = serviceDiscoveryProvider; + ServiceProviderHost = serviceProviderHost; + ServiceProviderPort = serviceProviderPort; + } + + public string ServiceName { get; } + public string DownstreamHost { get; } + public int DownstreamPort { get; } + public bool UseServiceDiscovery { get; } + public string ServiceDiscoveryProvider { get; } + public string ServiceProviderHost { get; private set; } + public int ServiceProviderPort { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Setter/FileConfigurationSetter.cs b/src/Ocelot/Configuration/Setter/FileConfigurationSetter.cs new file mode 100644 index 00000000..93dd4d85 --- /dev/null +++ b/src/Ocelot/Configuration/Setter/FileConfigurationSetter.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Setter +{ + public class FileConfigurationSetter : IFileConfigurationSetter + { + private readonly IOcelotConfigurationRepository _configRepo; + private readonly IOcelotConfigurationCreator _configCreator; + private readonly IFileConfigurationRepository _repo; + + public FileConfigurationSetter(IOcelotConfigurationRepository configRepo, + IOcelotConfigurationCreator configCreator, IFileConfigurationRepository repo) + { + _configRepo = configRepo; + _configCreator = configCreator; + _repo = repo; + } + + public async Task Set(FileConfiguration fileConfig) + { + var response = _repo.Set(fileConfig); + + if(response.IsError) + { + return new ErrorResponse(response.Errors); + } + + var config = await _configCreator.Create(fileConfig); + + if(!config.IsError) + { + _configRepo.AddOrReplace(config.Data); + } + + return new ErrorResponse(config.Errors); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs b/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs new file mode 100644 index 00000000..8ab31d1b --- /dev/null +++ b/src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Ocelot.Configuration.File; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Setter +{ + public interface IFileConfigurationSetter + { + Task Set(FileConfiguration config); + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Validator/DownstreamPathTemplateAlreadyUsedError.cs b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateAlreadyUsedError.cs new file mode 100644 index 00000000..e350753c --- /dev/null +++ b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateAlreadyUsedError.cs @@ -0,0 +1,11 @@ +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class DownstreamPathTemplateAlreadyUsedError : Error + { + public DownstreamPathTemplateAlreadyUsedError(string message) : base(message, OcelotErrorCode.DownstreampathTemplateAlreadyUsedError) + { + } + } +} diff --git a/src/Ocelot/Configuration/Validator/DownstreamPathTemplateContainsSchemeError.cs b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateContainsSchemeError.cs new file mode 100644 index 00000000..a3dfa309 --- /dev/null +++ b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateContainsSchemeError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class DownstreamPathTemplateContainsSchemeError : Error + { + public DownstreamPathTemplateContainsSchemeError(string message) + : base(message, OcelotErrorCode.DownstreamPathTemplateContainsSchemeError) + { + } + } +} diff --git a/src/Ocelot/Configuration/Validator/DownstreamTemplateAlreadyUsedError.cs b/src/Ocelot/Configuration/Validator/DownstreamTemplateAlreadyUsedError.cs deleted file mode 100644 index b836b1eb..00000000 --- a/src/Ocelot/Configuration/Validator/DownstreamTemplateAlreadyUsedError.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Ocelot.Errors; - -namespace Ocelot.Configuration.Validator -{ - public class DownstreamTemplateAlreadyUsedError : Error - { - public DownstreamTemplateAlreadyUsedError(string message) : base(message, OcelotErrorCode.DownstreamTemplateAlreadyUsedError) - { - } - } -} diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs index fb55a10b..98301ab4 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 = CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(configuration); + + if (result.IsError) + { + return new OkResponse(result); + } + return new OkResponse(result); } @@ -47,7 +54,7 @@ namespace Ocelot.Configuration.Validator continue; } - var error = new UnsupportedAuthenticationProviderError($"{reRoute.AuthenticationOptions?.Provider} is unsupported authentication provider, upstream template is {reRoute.UpstreamTemplate}, upstream method is {reRoute.UpstreamHttpMethod}"); + var error = new UnsupportedAuthenticationProviderError($"{reRoute.AuthenticationOptions?.Provider} is unsupported authentication provider, upstream template is {reRoute.UpstreamPathTemplate}, upstream method is {reRoute.UpstreamHttpMethod}"); errors.Add(error); } @@ -63,21 +70,42 @@ namespace Ocelot.Configuration.Validator return Enum.TryParse(provider, true, out supportedProvider); } + private ConfigurationValidationResult CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(FileConfiguration configuration) + { + var errors = new List(); + + foreach(var reRoute in configuration.ReRoutes) + { + if(reRoute.DownstreamPathTemplate.Contains("https://") + || reRoute.DownstreamPathTemplate.Contains("http://")) + { + errors.Add(new DownstreamPathTemplateContainsSchemeError($"{reRoute.DownstreamPathTemplate} contains scheme")); + } + } + + if(errors.Any()) + { + return new ConfigurationValidationResult(true, errors); + } + + return new ConfigurationValidationResult(false, errors); + } + private ConfigurationValidationResult CheckForDupliateReRoutes(FileConfiguration configuration) { var hasDupes = configuration.ReRoutes - .GroupBy(x => new { x.UpstreamTemplate, x.UpstreamHttpMethod }).Any(x => x.Skip(1).Any()); + .GroupBy(x => new { x.UpstreamPathTemplate, x.UpstreamHttpMethod }).Any(x => x.Skip(1).Any()); if (!hasDupes) { return new ConfigurationValidationResult(false); } - var dupes = configuration.ReRoutes.GroupBy(x => new { x.UpstreamTemplate, x.UpstreamHttpMethod }) + var dupes = configuration.ReRoutes.GroupBy(x => new { x.UpstreamPathTemplate, x.UpstreamHttpMethod }) .Where(x => x.Skip(1).Any()); var errors = dupes - .Select(d => new DownstreamTemplateAlreadyUsedError(string.Format("Duplicate DownstreamTemplate: {0}", d.Key.UpstreamTemplate))) + .Select(d => new DownstreamPathTemplateAlreadyUsedError(string.Format("Duplicate DownstreamPath: {0}", d.Key.UpstreamPathTemplate))) .Cast() .ToList(); diff --git a/src/Ocelot/Controllers/FileConfigurationController.cs b/src/Ocelot/Controllers/FileConfigurationController.cs new file mode 100644 index 00000000..5e36c64b --- /dev/null +++ b/src/Ocelot/Controllers/FileConfigurationController.cs @@ -0,0 +1,49 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Provider; +using Ocelot.Configuration.Setter; + +namespace Ocelot.Controllers +{ + [Authorize] + [Route("configuration")] + public class FileConfigurationController : Controller + { + private readonly IFileConfigurationProvider _configGetter; + private readonly IFileConfigurationSetter _configSetter; + + public FileConfigurationController(IFileConfigurationProvider getFileConfig, IFileConfigurationSetter configSetter) + { + _configGetter = getFileConfig; + _configSetter = configSetter; + } + + [HttpGet] + public IActionResult Get() + { + var response = _configGetter.Get(); + + if(response.IsError) + { + return new BadRequestObjectResult(response.Errors); + } + + return new OkObjectResult(response.Data); + } + + [HttpPost] + public async Task Post([FromBody]FileConfiguration fileConfiguration) + { + var response = await _configSetter.Set(fileConfiguration); + + if(response.IsError) + { + return new BadRequestObjectResult(response.Errors); + } + + return new OkObjectResult(fileConfiguration); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs index 04839abb..c076f367 100644 --- a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -1,64 +1,121 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Net.Http; using CacheManager.Core; +using IdentityServer4.Models; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Ocelot.Authentication.Handler.Creator; using Ocelot.Authentication.Handler.Factory; using Ocelot.Authorisation; using Ocelot.Cache; using Ocelot.Claims; +using Ocelot.Configuration.Authentication; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Provider; using Ocelot.Configuration.Repository; +using Ocelot.Configuration.Setter; using Ocelot.Configuration.Validator; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator; using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; using Ocelot.Headers; using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Infrastructure.RequestData; +using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Logging; +using Ocelot.Middleware; using Ocelot.QueryStrings; using Ocelot.Request.Builder; using Ocelot.Requester; +using Ocelot.Requester.QoS; using Ocelot.Responder; +using Ocelot.ServiceDiscovery; +using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider; +using Ocelot.RateLimit; namespace Ocelot.DependencyInjection { public static class ServiceCollectionExtensions { - public static IServiceCollection AddOcelotOutputCaching(this IServiceCollection services, Action settings) { var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", settings); var ocelotCacheManager = new OcelotCacheManagerCache(cacheManagerOutputCache); - services.AddSingleton>(cacheManagerOutputCache); services.AddSingleton>(ocelotCacheManager); return services; } - public static IServiceCollection AddOcelotFileConfiguration(this IServiceCollection services, IConfigurationRoot configurationRoot) + + public static IServiceCollection AddOcelot(this IServiceCollection services, IConfigurationRoot configurationRoot) { services.Configure(configurationRoot); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); - return services; - } - - public static IServiceCollection AddOcelot(this IServiceCollection services) - { - services.AddMvcCore().AddJsonFormatters(); + var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(); + + if(identityServerConfiguration != null) + { + services.AddSingleton(identityServerConfiguration); + services.AddSingleton(); + services.AddIdentityServer() + .AddTemporarySigningCredential() + .AddInMemoryApiResources(new List + { + new ApiResource + { + Name = identityServerConfiguration.ApiName, + Description = identityServerConfiguration.Description, + Enabled = identityServerConfiguration.Enabled, + DisplayName = identityServerConfiguration.ApiName, + Scopes = identityServerConfiguration.AllowedScopes.Select(x => new Scope(x)).ToList(), + ApiSecrets = new List + { + new Secret + { + Value = identityServerConfiguration.ApiSecret.Sha256() + } + } + } + }) + .AddInMemoryClients(new List + { + new Client + { + ClientId = identityServerConfiguration.ApiName, + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new Secret(identityServerConfiguration.ApiSecret.Sha256())}, + AllowedScopes = identityServerConfiguration.AllowedScopes, + AccessTokenType = identityServerConfiguration.AccessTokenType, + Enabled = identityServerConfiguration.Enabled, + RequireClientSecret = identityServerConfiguration.RequireClientSecret + } + }).AddResourceOwnerValidator(); + } + + services.AddMvcCore() + .AddAuthorization() + .AddJsonFormatters(); services.AddLogging(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -69,7 +126,7 @@ namespace Ocelot.DependencyInjection services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -77,12 +134,13 @@ namespace Ocelot.DependencyInjection services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc // could maybe use a scoped data repository services.AddSingleton(); services.AddScoped(); - + services.AddMemoryCache(); return services; } } diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs.orig b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs.orig new file mode 100644 index 00000000..86f7be59 --- /dev/null +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs.orig @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using CacheManager.Core; +using IdentityServer4.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Authentication.Handler.Creator; +using Ocelot.Authentication.Handler.Factory; +using Ocelot.Authorisation; +using Ocelot.Cache; +using Ocelot.Claims; +using Ocelot.Configuration.Authentication; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Parser; +using Ocelot.Configuration.Provider; +using Ocelot.Configuration.Repository; +using Ocelot.Configuration.Setter; +using Ocelot.Configuration.Validator; +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Headers; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Infrastructure.RequestData; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.QueryStrings; +using Ocelot.Request.Builder; +using Ocelot.Requester; +using Ocelot.Requester.QoS; +using Ocelot.Responder; +using Ocelot.ServiceDiscovery; +<<<<<<< HEAD +using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider; +======= +using Ocelot.RateLimit; +>>>>>>> develop + +namespace Ocelot.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddOcelotOutputCaching(this IServiceCollection services, Action settings) + { + var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", settings); + var ocelotCacheManager = new OcelotCacheManagerCache(cacheManagerOutputCache); + services.AddSingleton>(cacheManagerOutputCache); + services.AddSingleton>(ocelotCacheManager); + + return services; + } + + public static IServiceCollection AddOcelot(this IServiceCollection services, IConfigurationRoot configurationRoot) + { + services.Configure(configurationRoot); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration(); + + if(identityServerConfiguration != null) + { + services.AddSingleton(identityServerConfiguration); + services.AddSingleton(); + services.AddIdentityServer() + .AddTemporarySigningCredential() + .AddInMemoryApiResources(new List + { + new ApiResource + { + Name = identityServerConfiguration.ApiName, + Description = identityServerConfiguration.Description, + Enabled = identityServerConfiguration.Enabled, + DisplayName = identityServerConfiguration.ApiName, + Scopes = identityServerConfiguration.AllowedScopes.Select(x => new Scope(x)).ToList(), + ApiSecrets = new List + { + new Secret + { + Value = identityServerConfiguration.ApiSecret.Sha256() + } + } + } + }) + .AddInMemoryClients(new List + { + new Client + { + ClientId = identityServerConfiguration.ApiName, + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new Secret(identityServerConfiguration.ApiSecret.Sha256())}, + AllowedScopes = identityServerConfiguration.AllowedScopes, + AccessTokenType = identityServerConfiguration.AccessTokenType, + Enabled = identityServerConfiguration.Enabled, + RequireClientSecret = identityServerConfiguration.RequireClientSecret + } + }).AddResourceOwnerValidator(); + } + + services.AddMvcCore() + .AddAuthorization() + .AddJsonFormatters(); + services.AddLogging(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc + // could maybe use a scoped data repository + services.AddSingleton(); + services.AddScoped(); + services.AddMemoryCache(); + return services; + } + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index 752da281..6773feb8 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -25,15 +25,22 @@ namespace Ocelot.DownstreamRouteFinder.Finder { var configuration = _configProvider.Get(); - var applicableReRoutes = configuration.Data.ReRoutes.Where(r => string.Equals(r.UpstreamHttpMethod, upstreamHttpMethod, StringComparison.CurrentCultureIgnoreCase)); + var applicableReRoutes = configuration.Data.ReRoutes.Where(r => string.Equals(r.UpstreamHttpMethod.Method, upstreamHttpMethod, StringComparison.CurrentCultureIgnoreCase)); foreach (var reRoute in applicableReRoutes) { + if (upstreamUrlPath == reRoute.UpstreamTemplatePattern) + { + var templateVariableNameAndValues = _urlPathPlaceholderNameAndValueFinder.Find(upstreamUrlPath, reRoute.UpstreamPathTemplate.Value); + + return new OkResponse(new DownstreamRoute(templateVariableNameAndValues.Data, reRoute)); + } + var urlMatch = _urlMatcher.Match(upstreamUrlPath, reRoute.UpstreamTemplatePattern); if (urlMatch.Data.Match) { - var templateVariableNameAndValues = _urlPathPlaceholderNameAndValueFinder.Find(upstreamUrlPath, reRoute.UpstreamTemplate); + var templateVariableNameAndValues = _urlPathPlaceholderNameAndValueFinder.Find(upstreamUrlPath, reRoute.UpstreamPathTemplate.Value); return new OkResponse(new DownstreamRoute(templateVariableNameAndValues.Data, reRoute)); } diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index a74b6316..02e3e4f6 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; @@ -44,7 +43,7 @@ namespace Ocelot.DownstreamRouteFinder.Middleware return; } - _logger.LogDebug("downstream template is {downstreamRoute.Data.ReRoute.DownstreamTemplate}", downstreamRoute.Data.ReRoute.DownstreamTemplate); + _logger.LogDebug("downstream template is {downstreamRoute.Data.ReRoute.DownstreamPath}", downstreamRoute.Data.ReRoute.DownstreamPathTemplate); SetDownstreamRouteForThisRequest(downstreamRoute.Data); diff --git a/src/Ocelot/DownstreamUrlCreator/DownstreamHostNullOrEmptyError.cs b/src/Ocelot/DownstreamUrlCreator/DownstreamHostNullOrEmptyError.cs new file mode 100644 index 00000000..8978f665 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/DownstreamHostNullOrEmptyError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.DownstreamUrlCreator +{ + public class DownstreamHostNullOrEmptyError : Error + { + public DownstreamHostNullOrEmptyError() + : base("downstream host was null or empty", OcelotErrorCode.DownstreamHostNullOrEmptyError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/DownstreamPathNullOrEmptyError.cs b/src/Ocelot/DownstreamUrlCreator/DownstreamPathNullOrEmptyError.cs new file mode 100644 index 00000000..fbc1a5f5 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/DownstreamPathNullOrEmptyError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.DownstreamUrlCreator +{ + public class DownstreamPathNullOrEmptyError : Error + { + public DownstreamPathNullOrEmptyError() + : base("downstream path was null or empty", OcelotErrorCode.DownstreamPathNullOrEmptyError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/DownstreamSchemeNullOrEmptyError.cs b/src/Ocelot/DownstreamUrlCreator/DownstreamSchemeNullOrEmptyError.cs new file mode 100644 index 00000000..e52d3488 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/DownstreamSchemeNullOrEmptyError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.DownstreamUrlCreator +{ + public class DownstreamSchemeNullOrEmptyError : Error + { + public DownstreamSchemeNullOrEmptyError() + : base("downstream scheme was null or empty", OcelotErrorCode.DownstreamSchemeNullOrEmptyError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/IUrlBuilder.cs b/src/Ocelot/DownstreamUrlCreator/IUrlBuilder.cs new file mode 100644 index 00000000..8a07a066 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/IUrlBuilder.cs @@ -0,0 +1,10 @@ +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.DownstreamUrlCreator +{ + public interface IUrlBuilder + { + Response Build(string downstreamPath, string downstreamScheme, HostAndPort downstreamHostAndPort); + } +} diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs index 8d0af0bd..631e278a 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; @@ -11,17 +10,20 @@ namespace Ocelot.DownstreamUrlCreator.Middleware public class DownstreamUrlCreatorMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; - private readonly IDownstreamUrlPathPlaceholderReplacer _urlReplacer; + private readonly IDownstreamPathPlaceholderReplacer _replacer; private readonly IOcelotLogger _logger; + private readonly IUrlBuilder _urlBuilder; public DownstreamUrlCreatorMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, - IDownstreamUrlPathPlaceholderReplacer urlReplacer, - IRequestScopedDataRepository requestScopedDataRepository) + IDownstreamPathPlaceholderReplacer replacer, + IRequestScopedDataRepository requestScopedDataRepository, + IUrlBuilder urlBuilder) :base(requestScopedDataRepository) { _next = next; - _urlReplacer = urlReplacer; + _replacer = replacer; + _urlBuilder = urlBuilder; _logger = loggerFactory.CreateLogger(); } @@ -29,19 +31,34 @@ namespace Ocelot.DownstreamUrlCreator.Middleware { _logger.LogDebug("started calling downstream url creator middleware"); - var downstreamUrl = _urlReplacer.Replace(DownstreamRoute.ReRoute.DownstreamTemplate, DownstreamRoute.TemplatePlaceholderNameAndValues); + var dsPath = _replacer + .Replace(DownstreamRoute.ReRoute.DownstreamPathTemplate, DownstreamRoute.TemplatePlaceholderNameAndValues); - if (downstreamUrl.IsError) + if (dsPath.IsError) { - _logger.LogDebug("IDownstreamUrlPathPlaceholderReplacer returned an error, setting pipeline error"); + _logger.LogDebug("IDownstreamPathPlaceholderReplacer returned an error, setting pipeline error"); - SetPipelineError(downstreamUrl.Errors); + SetPipelineError(dsPath.Errors); return; } - _logger.LogDebug("downstream url is {downstreamUrl.Data.Value}", downstreamUrl.Data.Value); + var dsScheme = DownstreamRoute.ReRoute.DownstreamScheme; + + var dsHostAndPort = HostAndPort; - SetDownstreamUrlForThisRequest(downstreamUrl.Data.Value); + var dsUrl = _urlBuilder.Build(dsPath.Data.Value, dsScheme, dsHostAndPort); + + if (dsUrl.IsError) + { + _logger.LogDebug("IUrlBuilder returned an error, setting pipeline error"); + + SetPipelineError(dsUrl.Errors); + return; + } + + _logger.LogDebug("downstream url is {downstreamUrl.Data.Value}", dsUrl.Data.Value); + + SetDownstreamUrlForThisRequest(dsUrl.Data.Value); _logger.LogDebug("calling next middleware"); diff --git a/src/Ocelot/DownstreamUrlCreator/UrlBuilder.cs b/src/Ocelot/DownstreamUrlCreator/UrlBuilder.cs new file mode 100644 index 00000000..43a63715 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/UrlBuilder.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Ocelot.Errors; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.DownstreamUrlCreator +{ + public class UrlBuilder : IUrlBuilder + { + public Response Build(string downstreamPath, string downstreamScheme, HostAndPort downstreamHostAndPort) + { + if (string.IsNullOrEmpty(downstreamPath)) + { + return new ErrorResponse(new List {new DownstreamPathNullOrEmptyError()}); + } + + if (string.IsNullOrEmpty(downstreamScheme)) + { + return new ErrorResponse(new List { new DownstreamSchemeNullOrEmptyError() }); + } + + if (string.IsNullOrEmpty(downstreamHostAndPort.DownstreamHost)) + { + return new ErrorResponse(new List { new DownstreamHostNullOrEmptyError() }); + } + + var builder = new UriBuilder + { + Host = downstreamHostAndPort.DownstreamHost, + Path = downstreamPath, + Scheme = downstreamScheme + }; + + if (downstreamHostAndPort.DownstreamPort > 0) + { + builder.Port = downstreamHostAndPort.DownstreamPort; + } + + var url = builder.Uri.ToString(); + + return new OkResponse(new DownstreamUrl(url)); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs index 9c19f2f9..3c42b4f4 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs +++ b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs @@ -1,25 +1,25 @@ using System.Collections.Generic; using System.Text; -using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; +using Ocelot.Values; namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer { - public class DownstreamUrlPathPlaceholderReplacer : IDownstreamUrlPathPlaceholderReplacer + public class DownstreamTemplatePathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer { - public Response Replace(string downstreamTemplate, List urlPathPlaceholderNameAndValues) + public Response Replace(PathTemplate downstreamPathTemplate, List urlPathPlaceholderNameAndValues) { - var upstreamUrl = new StringBuilder(); + var downstreamPath = new StringBuilder(); - upstreamUrl.Append(downstreamTemplate); + downstreamPath.Append(downstreamPathTemplate.Value); foreach (var placeholderVariableAndValue in urlPathPlaceholderNameAndValues) { - upstreamUrl.Replace(placeholderVariableAndValue.TemplateVariableName, placeholderVariableAndValue.TemplateVariableValue); + downstreamPath.Replace(placeholderVariableAndValue.TemplateVariableName, placeholderVariableAndValue.TemplateVariableValue); } - return new OkResponse(new DownstreamUrl(upstreamUrl.ToString())); + return new OkResponse(new DownstreamPath(downstreamPath.ToString())); } } } \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs index 164c42ef..647af63a 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs +++ b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; +using Ocelot.Values; namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer { - public interface IDownstreamUrlPathPlaceholderReplacer + public interface IDownstreamPathPlaceholderReplacer { - Response Replace(string downstreamTemplate, List urlPathPlaceholderNameAndValues); + Response Replace(PathTemplate downstreamPathTemplate, List urlPathPlaceholderNameAndValues); } } \ No newline at end of file diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index fc3ab200..ea5d2c84 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -1,8 +1,6 @@ using System; -using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; @@ -41,13 +39,13 @@ namespace Ocelot.Errors.Middleware var message = CreateMessage(context, e); _logger.LogError(message, e); - await SetInternalServerErrorOnResponse(context); + SetInternalServerErrorOnResponse(context); } _logger.LogDebug("ocelot pipeline finished"); } - private async Task SetInternalServerErrorOnResponse(HttpContext context) + private void SetInternalServerErrorOnResponse(HttpContext context) { context.Response.OnStarting(x => { diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index a4864d1a..373ac441 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -4,7 +4,7 @@ { UnauthenticatedError, UnknownError, - DownstreamTemplateAlreadyUsedError, + DownstreampathTemplateAlreadyUsedError, UnableToFindDownstreamRouteError, CannotAddDataError, CannotFindDataError, @@ -17,6 +17,16 @@ InstructionNotForClaimsError, UnauthorizedError, ClaimValueNotAuthorisedError, - UserDoesNotHaveClaimError + UserDoesNotHaveClaimError, + DownstreamPathTemplateContainsSchemeError, + DownstreamPathNullOrEmptyError, + DownstreamSchemeNullOrEmptyError, + DownstreamHostNullOrEmptyError, + ServicesAreNullError, + ServicesAreEmptyError, + UnableToFindServiceDiscoveryProviderError, + UnableToFindLoadBalancerError, + RequestTimedOutError, + UnableToFindQoSProviderError } } diff --git a/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs index cc262048..a89d2ec2 100644 --- a/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; diff --git a/src/Ocelot/Headers/RemoveOutputHeaders.cs b/src/Ocelot/Headers/RemoveOutputHeaders.cs index 7a40534b..f3020036 100644 --- a/src/Ocelot/Headers/RemoveOutputHeaders.cs +++ b/src/Ocelot/Headers/RemoveOutputHeaders.cs @@ -3,8 +3,6 @@ using Ocelot.Responses; namespace Ocelot.Headers { - using System; - public class RemoveOutputHeaders : IRemoveOutputHeaders { /// diff --git a/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs b/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs new file mode 100644 index 00000000..d7458381 --- /dev/null +++ b/src/Ocelot/Infrastructure/Extensions/StringExtensions.cs @@ -0,0 +1,24 @@ +using System; + +namespace Ocelot.Infrastructure.Extensions +{ + public static class StringExtensions + { + public static string TrimStart(this string source, string trim, StringComparison stringComparison = StringComparison.Ordinal) + { + if (source == null) + { + return null; + } + + string s = source; + while (s.StartsWith(trim, stringComparison)) + { + s = s.Substring(trim.Length); + } + + return s; + } + + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs new file mode 100644 index 00000000..77dc328d --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancer.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancer + { + Task> Lease(); + void Release(HostAndPort hostAndPort); + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs new file mode 100644 index 00000000..19fdf3eb --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerFactory.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Ocelot.Configuration; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancerFactory + { + Task Get(ReRoute reRoute); + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs new file mode 100644 index 00000000..065ae2ac --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ILoadBalancerHouse.cs @@ -0,0 +1,10 @@ +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public interface ILoadBalancerHouse + { + Response Get(string key); + Response Add(string key, ILoadBalancer loadBalancer); + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/Lease.cs b/src/Ocelot/LoadBalancer/LoadBalancers/Lease.cs new file mode 100644 index 00000000..bd2e1b88 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/Lease.cs @@ -0,0 +1,15 @@ +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class Lease + { + public Lease(HostAndPort hostAndPort, int connections) + { + HostAndPort = hostAndPort; + Connections = connections; + } + public HostAndPort HostAndPort { get; private set; } + public int Connections { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionLoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionLoadBalancer.cs new file mode 100644 index 00000000..cd56ef91 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnectionLoadBalancer.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Errors; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LeastConnectionLoadBalancer : ILoadBalancer + { + private readonly Func>> _services; + private readonly List _leases; + private readonly string _serviceName; + private static readonly object _syncLock = new object(); + + public LeastConnectionLoadBalancer(Func>> services, string serviceName) + { + _services = services; + _serviceName = serviceName; + _leases = new List(); + } + + public async Task> Lease() + { + var services = await _services.Invoke(); + + if (services == null) + { + return new ErrorResponse(new List() { new ServicesAreNullError($"services were null for {_serviceName}") }); + } + + if (!services.Any()) + { + return new ErrorResponse(new List() { new ServicesAreEmptyError($"services were empty for {_serviceName}") }); + } + + lock(_syncLock) + { + //todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something? + UpdateServices(services); + + var leaseWithLeastConnections = GetLeaseWithLeastConnections(); + + _leases.Remove(leaseWithLeastConnections); + + leaseWithLeastConnections = AddConnection(leaseWithLeastConnections); + + _leases.Add(leaseWithLeastConnections); + + return new OkResponse(new HostAndPort(leaseWithLeastConnections.HostAndPort.DownstreamHost, leaseWithLeastConnections.HostAndPort.DownstreamPort)); + } + } + + public void Release(HostAndPort hostAndPort) + { + lock(_syncLock) + { + var matchingLease = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == hostAndPort.DownstreamHost + && l.HostAndPort.DownstreamPort == hostAndPort.DownstreamPort); + + if (matchingLease != null) + { + var replacementLease = new Lease(hostAndPort, matchingLease.Connections - 1); + + _leases.Remove(matchingLease); + + _leases.Add(replacementLease); + } + } + } + + private Lease AddConnection(Lease lease) + { + return new Lease(lease.HostAndPort, lease.Connections + 1); + } + + private Lease GetLeaseWithLeastConnections() + { + //now get the service with the least connections? + Lease leaseWithLeastConnections = null; + + for (var i = 0; i < _leases.Count; i++) + { + if (i == 0) + { + leaseWithLeastConnections = _leases[i]; + } + else + { + if (_leases[i].Connections < leaseWithLeastConnections.Connections) + { + leaseWithLeastConnections = _leases[i]; + } + } + } + + return leaseWithLeastConnections; + } + + private Response UpdateServices(List services) + { + if (_leases.Count > 0) + { + var leasesToRemove = new List(); + + foreach (var lease in _leases) + { + var match = services.FirstOrDefault(s => s.HostAndPort.DownstreamHost == lease.HostAndPort.DownstreamHost + && s.HostAndPort.DownstreamPort == lease.HostAndPort.DownstreamPort); + + if (match == null) + { + leasesToRemove.Add(lease); + } + } + + foreach (var lease in leasesToRemove) + { + _leases.Remove(lease); + } + + foreach (var service in services) + { + var exists = _leases.FirstOrDefault(l => l.HostAndPort.ToString() == service.HostAndPort.ToString()); + + if (exists == null) + { + _leases.Add(new Lease(service.HostAndPort, 0)); + } + } + } + else + { + foreach (var service in services) + { + _leases.Add(new Lease(service.HostAndPort, 0)); + } + } + + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs new file mode 100644 index 00000000..72ccdba1 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerFactory.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Ocelot.Configuration; +using Ocelot.ServiceDiscovery; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LoadBalancerFactory : ILoadBalancerFactory + { + private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory; + public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory) + { + _serviceProviderFactory = serviceProviderFactory; + } + + public async Task Get(ReRoute reRoute) + { + var serviceProvider = _serviceProviderFactory.Get(reRoute.ServiceProviderConfiguraion); + + switch (reRoute.LoadBalancer) + { + case "RoundRobin": + return new RoundRobinLoadBalancer(await serviceProvider.Get()); + case "LeastConnection": + return new LeastConnectionLoadBalancer(async () => await serviceProvider.Get(), reRoute.ServiceProviderConfiguraion.ServiceName); + default: + return new NoLoadBalancer(await serviceProvider.Get()); + } + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs new file mode 100644 index 00000000..68e42fe2 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LoadBalancerHouse.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using Ocelot.Responses; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class LoadBalancerHouse : ILoadBalancerHouse + { + private readonly Dictionary _loadBalancers; + + public LoadBalancerHouse() + { + _loadBalancers = new Dictionary(); + } + + public Response Get(string key) + { + ILoadBalancer loadBalancer; + + if(_loadBalancers.TryGetValue(key, out loadBalancer)) + { + return new OkResponse(_loadBalancers[key]); + } + + return new ErrorResponse(new List() + { + new UnableToFindLoadBalancerError($"unabe to find load balancer for {key}") + }); + } + + public Response Add(string key, ILoadBalancer loadBalancer) + { + if (!_loadBalancers.ContainsKey(key)) + { + _loadBalancers.Add(key, loadBalancer); + } + + _loadBalancers.Remove(key); + _loadBalancers.Add(key, loadBalancer); + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs new file mode 100644 index 00000000..bf66950b --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/NoLoadBalancer.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class NoLoadBalancer : ILoadBalancer + { + private readonly List _services; + + public NoLoadBalancer(List services) + { + _services = services; + } + + public async Task> Lease() + { + var service = await Task.FromResult(_services.FirstOrDefault()); + return new OkResponse(service.HostAndPort); + } + + public void Release(HostAndPort hostAndPort) + { + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinLoadBalancer.cs b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinLoadBalancer.cs new file mode 100644 index 00000000..37efe22f --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/RoundRobinLoadBalancer.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class RoundRobinLoadBalancer : ILoadBalancer + { + private readonly List _services; + private int _last; + + public RoundRobinLoadBalancer(List services) + { + _services = services; + } + + public async Task> Lease() + { + if (_last >= _services.Count) + { + _last = 0; + } + + var next = await Task.FromResult(_services[_last]); + _last++; + return new OkResponse(next.HostAndPort); + } + + public void Release(HostAndPort hostAndPort) + { + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs new file mode 100644 index 00000000..2fc4d953 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreEmptyError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class ServicesAreEmptyError : Error + { + public ServicesAreEmptyError(string message) + : base(message, OcelotErrorCode.ServicesAreEmptyError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs new file mode 100644 index 00000000..8e1bb700 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/ServicesAreNullError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class ServicesAreNullError : Error + { + public ServicesAreNullError(string message) + : base(message, OcelotErrorCode.ServicesAreNullError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs new file mode 100644 index 00000000..3dd3ede4 --- /dev/null +++ b/src/Ocelot/LoadBalancer/LoadBalancers/UnableToFindLoadBalancerError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.LoadBalancer.LoadBalancers +{ + public class UnableToFindLoadBalancerError : Errors.Error + { + public UnableToFindLoadBalancerError(string message) + : base(message, OcelotErrorCode.UnableToFindLoadBalancerError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs new file mode 100644 index 00000000..8e26cbf2 --- /dev/null +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.Infrastructure.RequestData; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.QueryStrings.Middleware; + +namespace Ocelot.LoadBalancer.Middleware +{ + public class LoadBalancingMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IOcelotLogger _logger; + private readonly ILoadBalancerHouse _loadBalancerHouse; + + public LoadBalancingMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository, + ILoadBalancerHouse loadBalancerHouse) + : base(requestScopedDataRepository) + { + _next = next; + _logger = loggerFactory.CreateLogger(); + _loadBalancerHouse = loadBalancerHouse; + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started calling load balancing middleware"); + + var loadBalancer = _loadBalancerHouse.Get(DownstreamRoute.ReRoute.ReRouteKey); + if(loadBalancer.IsError) + { + SetPipelineError(loadBalancer.Errors); + return; + } + + var hostAndPort = await loadBalancer.Data.Lease(); + if(hostAndPort.IsError) + { + SetPipelineError(hostAndPort.Errors); + return; + } + + SetHostAndPortForThisRequest(hostAndPort.Data); + + _logger.LogDebug("calling next middleware"); + + try + { + await _next.Invoke(context); + + loadBalancer.Data.Release(hostAndPort.Data); + } + catch (Exception) + { + loadBalancer.Data.Release(hostAndPort.Data); + + _logger.LogDebug("error calling next middleware, exception will be thrown to global handler"); + throw; + } + + _logger.LogDebug("succesfully called next middleware"); + } + } +} diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddlewareExtensions.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddlewareExtensions.cs new file mode 100644 index 00000000..0d0224b8 --- /dev/null +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.LoadBalancer.Middleware +{ + public static class LoadBalancingMiddlewareExtensions + { + public static IApplicationBuilder UseLoadBalancingMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Middleware/BaseUrlFinder.cs b/src/Ocelot/Middleware/BaseUrlFinder.cs new file mode 100644 index 00000000..da1fda4e --- /dev/null +++ b/src/Ocelot/Middleware/BaseUrlFinder.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Hosting; + +namespace Ocelot.Middleware +{ + public class BaseUrlFinder : IBaseUrlFinder + { + private readonly IWebHostBuilder _webHostBuilder; + + public BaseUrlFinder(IWebHostBuilder webHostBuilder) + { + _webHostBuilder = webHostBuilder; + } + + public string Find() + { + var baseSchemeUrlAndPort = _webHostBuilder.GetSetting(WebHostDefaults.ServerUrlsKey); + + return string.IsNullOrEmpty(baseSchemeUrlAndPort) ? "http://localhost:5000" : baseSchemeUrlAndPort; + } + } +} diff --git a/src/Ocelot/Middleware/IBaseUrlFinder.cs b/src/Ocelot/Middleware/IBaseUrlFinder.cs new file mode 100644 index 00000000..df54c0fe --- /dev/null +++ b/src/Ocelot/Middleware/IBaseUrlFinder.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Middleware +{ + public interface IBaseUrlFinder + { + string Find(); + } +} \ No newline at end of file diff --git a/src/Ocelot/Middleware/OcelotMiddleware.cs b/src/Ocelot/Middleware/OcelotMiddleware.cs index 0bb51040..b8937108 100644 --- a/src/Ocelot/Middleware/OcelotMiddleware.cs +++ b/src/Ocelot/Middleware/OcelotMiddleware.cs @@ -3,6 +3,7 @@ using System.Net.Http; using Ocelot.DownstreamRouteFinder; using Ocelot.Errors; using Ocelot.Infrastructure.RequestData; +using Ocelot.Values; namespace Ocelot.Middleware { @@ -69,6 +70,20 @@ namespace Ocelot.Middleware } } + public HostAndPort HostAndPort + { + get + { + var hostAndPort = _requestScopedDataRepository.Get("HostAndPort"); + return hostAndPort.Data; + } + } + + public void SetHostAndPortForThisRequest(HostAndPort hostAndPort) + { + _requestScopedDataRepository.Add("HostAndPort", hostAndPort); + } + public void SetDownstreamRouteForThisRequest(DownstreamRoute downstreamRoute) { _requestScopedDataRepository.Add("DownstreamRoute", downstreamRoute); diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index dfa3b3f4..e51c0c88 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Builder; +using System.Collections.Generic; +using IdentityServer4.AccessTokenValidation; +using Microsoft.AspNetCore.Builder; using Ocelot.Authentication.Middleware; using Ocelot.Cache.Middleware; using Ocelot.Claims.Middleware; @@ -11,13 +13,21 @@ using Ocelot.Request.Middleware; using Ocelot.Requester.Middleware; using Ocelot.RequestId.Middleware; using Ocelot.Responder.Middleware; +using Ocelot.RateLimit.Middleware; namespace Ocelot.Middleware { using System; using System.Threading.Tasks; using Authorisation.Middleware; + using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; + using Microsoft.Extensions.Options; + using Ocelot.Configuration; + using Ocelot.Configuration.File; + using Ocelot.Configuration.Provider; + using Ocelot.Configuration.Setter; + using Ocelot.LoadBalancer.Middleware; public static class OcelotMiddlewareExtensions { @@ -26,9 +36,10 @@ namespace Ocelot.Middleware /// /// /// - public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder) + public static async Task UseOcelot(this IApplicationBuilder builder) { - builder.UseOcelot(new OcelotMiddlewareConfiguration()); + await builder.UseOcelot(new OcelotMiddlewareConfiguration()); + return builder; } @@ -38,8 +49,10 @@ namespace Ocelot.Middleware /// /// /// - public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder, OcelotMiddlewareConfiguration middlewareConfiguration) + public static async Task UseOcelot(this IApplicationBuilder builder, OcelotMiddlewareConfiguration middlewareConfiguration) { + await CreateAdministrationArea(builder); + // This is registered to catch any global exceptions that are not handled builder.UseExceptionHandlerMiddleware(); @@ -52,6 +65,9 @@ namespace Ocelot.Middleware // Then we get the downstream route information builder.UseDownstreamRouteFinderMiddleware(); + // We check whether the request is ratelimit, and if there is no continue processing + builder.UseRateLimiting(); + // Now we can look for the requestId builder.UseRequestIdMiddleware(); @@ -98,6 +114,9 @@ namespace Ocelot.Middleware // Now we can run any query string transformation logic builder.UseQueryStringBuilderMiddleware(); + // Get the load balancer for this request + builder.UseLoadBalancingMiddleware(); + // This takes the downstream route we retrieved earlier and replaces any placeholders with the variables that should be used builder.UseDownstreamUrlCreatorMiddleware(); @@ -114,6 +133,64 @@ namespace Ocelot.Middleware return builder; } + private static async Task CreateConfiguration(IApplicationBuilder builder) + { + var fileConfig = (IOptions)builder.ApplicationServices.GetService(typeof(IOptions)); + + var configSetter = (IFileConfigurationSetter)builder.ApplicationServices.GetService(typeof(IFileConfigurationSetter)); + + var configProvider = (IOcelotConfigurationProvider)builder.ApplicationServices.GetService(typeof(IOcelotConfigurationProvider)); + + var config = await configSetter.Set(fileConfig.Value); + + if(config == null || config.IsError) + { + throw new Exception("Unable to start Ocelot: configuration was not set up correctly."); + } + + var ocelotConfiguration = configProvider.Get(); + + if(ocelotConfiguration == null || ocelotConfiguration.Data == null || ocelotConfiguration.IsError) + { + throw new Exception("Unable to start Ocelot: ocelot configuration was not returned by provider."); + } + + return ocelotConfiguration.Data; + } + + private static async Task CreateAdministrationArea(IApplicationBuilder builder) + { + var configuration = await CreateConfiguration(builder); + + var identityServerConfiguration = (IIdentityServerConfiguration)builder.ApplicationServices.GetService(typeof(IIdentityServerConfiguration)); + + if(!string.IsNullOrEmpty(configuration.AdministrationPath) && identityServerConfiguration != null) + { + var urlFinder = (IBaseUrlFinder)builder.ApplicationServices.GetService(typeof(IBaseUrlFinder)); + + var baseSchemeUrlAndPort = urlFinder.Find(); + + builder.Map(configuration.AdministrationPath, app => + { + var identityServerUrl = $"{baseSchemeUrlAndPort}/{configuration.AdministrationPath.Remove(0,1)}"; + + app.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions + { + Authority = identityServerUrl, + ApiName = identityServerConfiguration.ApiName, + RequireHttpsMetadata = identityServerConfiguration.RequireHttps, + AllowedScopes = identityServerConfiguration.AllowedScopes, + SupportedTokens = SupportedTokens.Both, + ApiSecret = identityServerConfiguration.ApiSecret + }); + + app.UseIdentityServer(); + + app.UseMvc(); + }); + } + } + private static void UseIfNotNull(this IApplicationBuilder builder, Func, Task> middleware) { if (middleware != null) diff --git a/src/Ocelot/Properties/AssemblyInfo.cs b/src/Ocelot/Properties/AssemblyInfo.cs index ad12027e..dd8b7610 100644 --- a/src/Ocelot/Properties/AssemblyInfo.cs +++ b/src/Ocelot/Properties/AssemblyInfo.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following diff --git a/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs b/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs index 1424d713..edeee51c 100644 --- a/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs +++ b/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs @@ -1,7 +1,6 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; diff --git a/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs new file mode 100644 index 00000000..a2ee1202 --- /dev/null +++ b/src/Ocelot/RateLimit/ClientRateLimitProcessor.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.RateLimit +{ + public class ClientRateLimitProcessor + { + private readonly IRateLimitCounterHandler _counterHandler; + private readonly RateLimitCore _core; + + public ClientRateLimitProcessor(IRateLimitCounterHandler counterHandler) + { + _counterHandler = counterHandler; + _core = new RateLimitCore(_counterHandler); + } + + public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) + { + return _core.ProcessRequest(requestIdentity, option); + } + + public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule) + { + return _core.RetryAfterFrom(timestamp, rule); + } + + public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) + { + return _core.GetRateLimitHeaders(context, requestIdentity, option); + } + + } +} diff --git a/src/Ocelot/RateLimit/ClientRequestIdentity.cs b/src/Ocelot/RateLimit/ClientRequestIdentity.cs new file mode 100644 index 00000000..a27bc994 --- /dev/null +++ b/src/Ocelot/RateLimit/ClientRequestIdentity.cs @@ -0,0 +1,18 @@ +namespace Ocelot.RateLimit +{ + public class ClientRequestIdentity + { + public ClientRequestIdentity(string clientId, string path, string httpverb) + { + ClientId = clientId; + Path = path; + HttpVerb = httpverb; + } + + public string ClientId { get; private set; } + + public string Path { get; private set; } + + public string HttpVerb { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs new file mode 100644 index 00000000..1db8f334 --- /dev/null +++ b/src/Ocelot/RateLimit/DistributedCacheRateLimitCounterHanlder.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Caching.Distributed; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.RateLimit +{ + public class DistributedCacheRateLimitCounterHanlder : IRateLimitCounterHandler + { + private readonly IDistributedCache _memoryCache; + + public DistributedCacheRateLimitCounterHanlder(IDistributedCache memoryCache) + { + _memoryCache = memoryCache; + } + + public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) + { + _memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); + } + + public bool Exists(string id) + { + var stored = _memoryCache.GetString(id); + return !string.IsNullOrEmpty(stored); + } + + public RateLimitCounter? Get(string id) + { + var stored = _memoryCache.GetString(id); + if (!string.IsNullOrEmpty(stored)) + { + return JsonConvert.DeserializeObject(stored); + } + return null; + } + + public void Remove(string id) + { + _memoryCache.Remove(id); + } + } +} diff --git a/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs new file mode 100644 index 00000000..cb745a44 --- /dev/null +++ b/src/Ocelot/RateLimit/IRateLimitCounterHandler.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.RateLimit +{ + public interface IRateLimitCounterHandler + { + bool Exists(string id); + RateLimitCounter? Get(string id); + void Remove(string id); + void Set(string id, RateLimitCounter counter, TimeSpan expirationTime); + } +} diff --git a/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs new file mode 100644 index 00000000..9756f2ae --- /dev/null +++ b/src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Caching.Memory; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.RateLimit +{ + public class MemoryCacheRateLimitCounterHandler : IRateLimitCounterHandler + { + private readonly IMemoryCache _memoryCache; + + public MemoryCacheRateLimitCounterHandler(IMemoryCache memoryCache) + { + _memoryCache = memoryCache; + } + + public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) + { + _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); + } + + public bool Exists(string id) + { + RateLimitCounter counter; + return _memoryCache.TryGetValue(id, out counter); + } + + public RateLimitCounter? Get(string id) + { + RateLimitCounter counter; + if (_memoryCache.TryGetValue(id, out counter)) + { + return counter; + } + + return null; + } + + public void Remove(string id) + { + _memoryCache.Remove(id); + } + } +} diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs new file mode 100644 index 00000000..1b3efee1 --- /dev/null +++ b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs @@ -0,0 +1,138 @@ +using Ocelot.Middleware; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Infrastructure.RequestData; +using Microsoft.AspNetCore.Http; +using Ocelot.Logging; +using Ocelot.Configuration; + +namespace Ocelot.RateLimit.Middleware +{ + public class ClientRateLimitMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IOcelotLogger _logger; + private readonly IRateLimitCounterHandler _counterHandler; + private readonly ClientRateLimitProcessor _processor; + + public ClientRateLimitMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository, + IRateLimitCounterHandler counterHandler) + : base(requestScopedDataRepository) + { + _next = next; + _logger = loggerFactory.CreateLogger(); + _counterHandler = counterHandler; + _processor = new ClientRateLimitProcessor(counterHandler); + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started calling RateLimit middleware"); + var options = DownstreamRoute.ReRoute.RateLimitOptions; + // check if rate limiting is enabled + if (!DownstreamRoute.ReRoute.EnableEndpointRateLimiting) + { + await _next.Invoke(context); + return; + } + // compute identity from request + var identity = SetIdentity(context, options); + + // check white list + if (IsWhitelisted(identity, options)) + { + await _next.Invoke(context); + return; + } + + var rule = options.RateLimitRule; + if (rule.Limit > 0) + { + // increment counter + var counter = _processor.ProcessRequest(identity, options); + + // check if limit is reached + if (counter.TotalRequests > rule.Limit) + { + //compute retry after value + var retryAfter = _processor.RetryAfterFrom(counter.Timestamp, rule); + + // log blocked request + LogBlockedRequest(context, identity, counter, rule); + + // break execution + await ReturnQuotaExceededResponse(context, options, retryAfter); + return; + } + } + //set X-Rate-Limit headers for the longest period + if (!options.DisableRateLimitHeaders) + { + var headers = _processor.GetRateLimitHeaders( context,identity, options); + context.Response.OnStarting(SetRateLimitHeaders, state: headers); + } + + await _next.Invoke(context); + } + + public virtual ClientRequestIdentity SetIdentity(HttpContext httpContext, RateLimitOptions option) + { + var clientId = "client"; + if (httpContext.Request.Headers.Keys.Contains(option.ClientIdHeader)) + { + clientId = httpContext.Request.Headers[option.ClientIdHeader].First(); + } + + return new ClientRequestIdentity( + clientId, + httpContext.Request.Path.ToString().ToLowerInvariant(), + httpContext.Request.Method.ToLowerInvariant() + ); + } + + public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOptions option) + { + if (option.ClientWhitelist.Contains(requestIdentity.ClientId)) + { + return true; + } + return false; + } + + public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule) + { + _logger.LogDebug($"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule { DownstreamRoute.ReRoute.UpstreamPathTemplate }, TraceIdentifier {httpContext.TraceIdentifier}."); + } + + public virtual Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter) + { + var message = string.IsNullOrEmpty(option.QuotaExceededMessage) ? $"API calls quota exceeded! maximum admitted {option.RateLimitRule.Limit} per {option.RateLimitRule.Period}." : option.QuotaExceededMessage; + + if (!option.DisableRateLimitHeaders) + { + httpContext.Response.Headers["Retry-After"] = retryAfter; + } + + httpContext.Response.StatusCode = option.HttpStatusCode; + return httpContext.Response.WriteAsync(message); + } + + private Task SetRateLimitHeaders(object rateLimitHeaders) + { + var headers = (RateLimitHeaders)rateLimitHeaders; + + headers.Context.Response.Headers["X-Rate-Limit-Limit"] = headers.Limit; + headers.Context.Response.Headers["X-Rate-Limit-Remaining"] = headers.Remaining; + headers.Context.Response.Headers["X-Rate-Limit-Reset"] = headers.Reset; + + return Task.CompletedTask; + } + + } +} + + diff --git a/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs new file mode 100644 index 00000000..b27d6d9d --- /dev/null +++ b/src/Ocelot/RateLimit/Middleware/RateLimitMiddlewareExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Builder; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.RateLimit.Middleware +{ + public static class RateLimitMiddlewareExtensions + { + public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/src/Ocelot/RateLimit/RateLimitCore.cs b/src/Ocelot/RateLimit/RateLimitCore.cs new file mode 100644 index 00000000..07627f8d --- /dev/null +++ b/src/Ocelot/RateLimit/RateLimitCore.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Ocelot.RateLimit +{ + public class RateLimitCore + { + private readonly IRateLimitCounterHandler _counterHandler; + private static readonly object _processLocker = new object(); + + public RateLimitCore(IRateLimitCounterHandler counterStore) + { + _counterHandler = counterStore; + } + + public RateLimitCounter ProcessRequest(ClientRequestIdentity requestIdentity, RateLimitOptions option) + { + RateLimitCounter counter = new RateLimitCounter(DateTime.UtcNow, 1); + var rule = option.RateLimitRule; + + var counterId = ComputeCounterKey(requestIdentity, option); + + // serial reads and writes + lock (_processLocker) + { + var entry = _counterHandler.Get(counterId); + if (entry.HasValue) + { + // entry has not expired + if (entry.Value.Timestamp + rule.PeriodTimespan >= DateTime.UtcNow) + { + // increment request count + var totalRequests = entry.Value.TotalRequests + 1; + + // deep copy + counter = new RateLimitCounter(entry.Value.Timestamp, totalRequests); + + } + } + // stores: id (string) - timestamp (datetime) - total_requests (long) + _counterHandler.Set(counterId, counter, rule.PeriodTimespan); + } + + return counter; + } + + public RateLimitHeaders GetRateLimitHeaders(HttpContext context, ClientRequestIdentity requestIdentity, RateLimitOptions option) + { + var rule = option.RateLimitRule; + RateLimitHeaders headers = null; + var counterId = ComputeCounterKey(requestIdentity, option); + var entry = _counterHandler.Get(counterId); + if (entry.HasValue) + { + headers = new RateLimitHeaders(context, rule.Period, + (rule.Limit - entry.Value.TotalRequests).ToString(), + (entry.Value.Timestamp + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo) + ); + } + else + { + headers = new RateLimitHeaders(context, + rule.Period, + rule.Limit.ToString(), + (DateTime.UtcNow + ConvertToTimeSpan(rule.Period)).ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); + + } + + return headers; + } + + public string ComputeCounterKey(ClientRequestIdentity requestIdentity, RateLimitOptions option) + { + var key = $"{option.RateLimitCounterPrefix}_{requestIdentity.ClientId}_{option.RateLimitRule.Period}_{requestIdentity.HttpVerb}_{requestIdentity.Path}"; + + var idBytes = Encoding.UTF8.GetBytes(key); + + byte[] hashBytes; + + using (var algorithm = SHA1.Create()) + { + hashBytes = algorithm.ComputeHash(idBytes); + } + + return BitConverter.ToString(hashBytes).Replace("-", string.Empty); + } + + public string RetryAfterFrom(DateTime timestamp, RateLimitRule rule) + { + var secondsPast = Convert.ToInt32((DateTime.UtcNow - timestamp).TotalSeconds); + var retryAfter = Convert.ToInt32(rule.PeriodTimespan.TotalSeconds); + retryAfter = retryAfter > 1 ? retryAfter - secondsPast : 1; + return retryAfter.ToString(System.Globalization.CultureInfo.InvariantCulture); + } + + public TimeSpan ConvertToTimeSpan(string timeSpan) + { + var l = timeSpan.Length - 1; + var value = timeSpan.Substring(0, l); + var type = timeSpan.Substring(l, 1); + + switch (type) + { + case "d": + return TimeSpan.FromDays(double.Parse(value)); + case "h": + return TimeSpan.FromHours(double.Parse(value)); + case "m": + return TimeSpan.FromMinutes(double.Parse(value)); + case "s": + return TimeSpan.FromSeconds(double.Parse(value)); + default: + throw new FormatException($"{timeSpan} can't be converted to TimeSpan, unknown type {type}"); + } + } + + } +} diff --git a/src/Ocelot/RateLimit/RateLimitCounter.cs b/src/Ocelot/RateLimit/RateLimitCounter.cs new file mode 100644 index 00000000..42dd03b7 --- /dev/null +++ b/src/Ocelot/RateLimit/RateLimitCounter.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.RateLimit +{ + /// + /// Stores the initial access time and the numbers of calls made from that point + /// + public struct RateLimitCounter + { + public RateLimitCounter(DateTime timestamp, long totalRequest) + { + Timestamp = timestamp; + TotalRequests = totalRequest; + } + + public DateTime Timestamp { get; private set; } + + public long TotalRequests { get; private set; } + } +} diff --git a/src/Ocelot/RateLimit/RateLimitHeaders.cs b/src/Ocelot/RateLimit/RateLimitHeaders.cs new file mode 100644 index 00000000..909f656d --- /dev/null +++ b/src/Ocelot/RateLimit/RateLimitHeaders.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Ocelot.RateLimit +{ + public class RateLimitHeaders + { + public RateLimitHeaders(HttpContext context, string limit, string remaining, string reset) + { + Context = context; + Limit = limit; + Remaining = remaining; + Reset = reset; + } + + public HttpContext Context { get; private set; } + + public string Limit { get; private set; } + + public string Remaining { get; private set; } + + public string Reset { get; private set; } + } +} diff --git a/src/Ocelot/Request/Builder/HttpRequestCreator.cs b/src/Ocelot/Request/Builder/HttpRequestCreator.cs index 91234331..3264db59 100644 --- a/src/Ocelot/Request/Builder/HttpRequestCreator.cs +++ b/src/Ocelot/Request/Builder/HttpRequestCreator.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Ocelot.Responses; +using Ocelot.Configuration; +using Ocelot.Requester.QoS; namespace Ocelot.Request.Builder { @@ -15,7 +17,9 @@ namespace Ocelot.Request.Builder IRequestCookieCollection cookies, QueryString queryString, string contentType, - RequestId.RequestId requestId) + RequestId.RequestId requestId, + bool isQos, + IQoSProvider qosProvider) { var request = await new RequestBuilder() .WithHttpMethod(httpMethod) @@ -26,6 +30,8 @@ namespace Ocelot.Request.Builder .WithHeaders(headers) .WithRequestId(requestId) .WithCookies(cookies) + .WithIsQos(isQos) + .WithQos(qosProvider) .Build(); return new OkResponse(request); diff --git a/src/Ocelot/Request/Builder/IRequestCreator.cs b/src/Ocelot/Request/Builder/IRequestCreator.cs index 7641d848..7db999b1 100644 --- a/src/Ocelot/Request/Builder/IRequestCreator.cs +++ b/src/Ocelot/Request/Builder/IRequestCreator.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Ocelot.Requester.QoS; using Ocelot.Responses; namespace Ocelot.Request.Builder @@ -13,7 +14,9 @@ namespace Ocelot.Request.Builder IHeaderDictionary headers, IRequestCookieCollection cookies, QueryString queryString, - string contentType, - RequestId.RequestId requestId); + string contentType, + RequestId.RequestId requestId, + bool isQos, + IQoSProvider qosProvider); } } diff --git a/src/Ocelot/Request/Builder/RequestBuilder.cs b/src/Ocelot/Request/Builder/RequestBuilder.cs index c9463993..ea6511ce 100644 --- a/src/Ocelot/Request/Builder/RequestBuilder.cs +++ b/src/Ocelot/Request/Builder/RequestBuilder.cs @@ -8,6 +8,7 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; +using Ocelot.Requester.QoS; namespace Ocelot.Request.Builder { @@ -22,6 +23,8 @@ namespace Ocelot.Request.Builder private RequestId.RequestId _requestId; private IRequestCookieCollection _cookies; private readonly string[] _unsupportedHeaders = {"host"}; + private bool _isQos; + private IQoSProvider _qoSProvider; public RequestBuilder WithHttpMethod(string httpMethod) { @@ -71,6 +74,18 @@ namespace Ocelot.Request.Builder return this; } + public RequestBuilder WithIsQos(bool isqos) + { + _isQos = isqos; + return this; + } + + public RequestBuilder WithQos(IQoSProvider qoSProvider) + { + _qoSProvider = qoSProvider; + return this; + } + public async Task Build() { var uri = CreateUri(); @@ -90,7 +105,7 @@ namespace Ocelot.Request.Builder var cookieContainer = CreateCookieContainer(uri); - return new Request(httpRequestMessage, cookieContainer); + return new Request(httpRequestMessage, cookieContainer,_isQos, _qoSProvider); } private Uri CreateUri() diff --git a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs index a2c5194b..991e84da 100644 --- a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs +++ b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs @@ -1,10 +1,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Builder; +using Ocelot.Requester.QoS; namespace Ocelot.Request.Middleware { @@ -13,15 +13,18 @@ namespace Ocelot.Request.Middleware private readonly RequestDelegate _next; private readonly IRequestCreator _requestCreator; private readonly IOcelotLogger _logger; + private readonly IQosProviderHouse _qosProviderHouse; public HttpRequestBuilderMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IRequestScopedDataRepository requestScopedDataRepository, - IRequestCreator requestCreator) + IRequestCreator requestCreator, + IQosProviderHouse qosProviderHouse) :base(requestScopedDataRepository) { _next = next; _requestCreator = requestCreator; + _qosProviderHouse = qosProviderHouse; _logger = loggerFactory.CreateLogger(); } @@ -29,16 +32,35 @@ namespace Ocelot.Request.Middleware { _logger.LogDebug("started calling request builder middleware"); + var qosProvider = _qosProviderHouse.Get(DownstreamRoute.ReRoute.ReRouteKey); + + if (qosProvider.IsError) + { + _logger.LogDebug("IQosProviderHouse returned an error, setting pipeline error"); + + SetPipelineError(qosProvider.Errors); + + return; + } + var buildResult = await _requestCreator - .Build(context.Request.Method, DownstreamUrl, context.Request.Body, - context.Request.Headers, context.Request.Cookies, context.Request.QueryString, - context.Request.ContentType, new RequestId.RequestId(DownstreamRoute?.ReRoute?.RequestIdKey, context.TraceIdentifier)); + .Build(context.Request.Method, + DownstreamUrl, + context.Request.Body, + context.Request.Headers, + context.Request.Cookies, + context.Request.QueryString, + context.Request.ContentType, + new RequestId.RequestId(DownstreamRoute?.ReRoute?.RequestIdKey, context.TraceIdentifier), + DownstreamRoute.ReRoute.IsQos, + qosProvider.Data); if (buildResult.IsError) { _logger.LogDebug("IRequestCreator returned an error, setting pipeline error"); SetPipelineError(buildResult.Errors); + return; } _logger.LogDebug("setting upstream request"); diff --git a/src/Ocelot/Request/Request.cs b/src/Ocelot/Request/Request.cs index d43071e1..680ab757 100644 --- a/src/Ocelot/Request/Request.cs +++ b/src/Ocelot/Request/Request.cs @@ -1,17 +1,28 @@ -using System.Net; +using Ocelot.Configuration; +using Ocelot.Values; +using System.Net; using System.Net.Http; +using Ocelot.Requester.QoS; namespace Ocelot.Request { public class Request { - public Request(HttpRequestMessage httpRequestMessage, CookieContainer cookieContainer) + public Request( + HttpRequestMessage httpRequestMessage, + CookieContainer cookieContainer, + bool isQos, + IQoSProvider qosProvider) { HttpRequestMessage = httpRequestMessage; CookieContainer = cookieContainer; + IsQos = isQos; + QosProvider = qosProvider; } public HttpRequestMessage HttpRequestMessage { get; private set; } public CookieContainer CookieContainer { get; private set; } + public bool IsQos { get; private set; } + public IQoSProvider QosProvider { get; private set; } } } diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs new file mode 100644 index 00000000..da37ee02 --- /dev/null +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using Ocelot.Logging; +using Ocelot.Requester.QoS; + +namespace Ocelot.Requester +{ + internal class HttpClientBuilder + { + private readonly Dictionary> _handlers = new Dictionary>(); + + public HttpClientBuilder WithQoS(IQoSProvider qoSProvider, IOcelotLogger logger, HttpMessageHandler innerHandler) + { + _handlers.Add(5000, () => new PollyCircuitBreakingDelegatingHandler(qoSProvider, logger, innerHandler)); + return this; + } + + internal HttpClient Build(HttpMessageHandler innerHandler) + { + return _handlers.Any() ? + new HttpClient(CreateHttpMessageHandler()) : + new HttpClient(innerHandler); + } + + private HttpMessageHandler CreateHttpMessageHandler() + { + HttpMessageHandler httpMessageHandler = new HttpClientHandler(); + + _handlers + .OrderByDescending(handler => handler.Key) + .Select(handler => handler.Value) + .Reverse() + .ToList() + .ForEach(handler => + { + var delegatingHandler = handler(); + delegatingHandler.InnerHandler = httpMessageHandler; + httpMessageHandler = delegatingHandler; + }); + + return httpMessageHandler; + } + } +} diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index 0167ef6a..5a2c86c7 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -1,31 +1,54 @@ using System; -using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; -using Ocelot.Errors; +using Ocelot.Logging; using Ocelot.Responses; +using Polly.CircuitBreaker; +using Polly.Timeout; namespace Ocelot.Requester { public class HttpClientHttpRequester : IHttpRequester { + private readonly IOcelotLogger _logger; + + public HttpClientHttpRequester(IOcelotLoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + public async Task> GetResponse(Request.Request request) { + var builder = new HttpClientBuilder(); + using (var handler = new HttpClientHandler { CookieContainer = request.CookieContainer }) - using (var httpClient = new HttpClient(handler)) { - try + if (request.IsQos) { - var response = await httpClient.SendAsync(request.HttpRequestMessage); - return new OkResponse(response); - } - catch (Exception exception) + builder.WithQoS(request.QosProvider, _logger, handler); + } + + using (var httpClient = builder.Build(handler)) { - return - new ErrorResponse(new List - { - new UnableToCompleteRequestError(exception) - }); + try + { + var response = await httpClient.SendAsync(request.HttpRequestMessage); + return new OkResponse(response); + } + catch (TimeoutRejectedException exception) + { + return + new ErrorResponse(new RequestTimedOutError(exception)); + } + catch (BrokenCircuitException exception) + { + return + new ErrorResponse(new RequestTimedOutError(exception)); + } + catch (Exception exception) + { + return new ErrorResponse(new UnableToCompleteRequestError(exception)); + } } } } diff --git a/src/Ocelot/Requester/IHttpRequester.cs b/src/Ocelot/Requester/IHttpRequester.cs index 201c3add..e3972957 100644 --- a/src/Ocelot/Requester/IHttpRequester.cs +++ b/src/Ocelot/Requester/IHttpRequester.cs @@ -7,5 +7,7 @@ namespace Ocelot.Requester public interface IHttpRequester { Task> GetResponse(Request.Request request); + + } } diff --git a/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs new file mode 100644 index 00000000..a50ec91b --- /dev/null +++ b/src/Ocelot/Requester/PollyCircuitBreakingDelegatingHandler.cs @@ -0,0 +1,47 @@ +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Ocelot.Logging; +using Ocelot.Requester.QoS; +using Polly; +using Polly.CircuitBreaker; +using Polly.Timeout; + +namespace Ocelot.Requester +{ + public class PollyCircuitBreakingDelegatingHandler : DelegatingHandler + { + private readonly IQoSProvider _qoSProvider; + private readonly IOcelotLogger _logger; + + public PollyCircuitBreakingDelegatingHandler( + IQoSProvider qoSProvider, + IOcelotLogger logger, + HttpMessageHandler innerHandler) + : base(innerHandler) + { + _qoSProvider = qoSProvider; + _logger = logger; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + try + { + return await Policy + .WrapAsync(_qoSProvider.CircuitBreaker.CircuitBreakerPolicy, _qoSProvider.CircuitBreaker.TimeoutPolicy) + .ExecuteAsync(() => base.SendAsync(request,cancellationToken)); + } + catch (BrokenCircuitException ex) + { + _logger.LogError($"Reached to allowed number of exceptions. Circuit is open",ex); + throw; + } + catch (HttpRequestException ex) + { + _logger.LogError($"Error in CircuitBreakingDelegatingHandler.SendAync", ex); + throw; + } + } + } +} diff --git a/src/Ocelot/Requester/QoS/IQoSProviderFactory.cs b/src/Ocelot/Requester/QoS/IQoSProviderFactory.cs new file mode 100644 index 00000000..d1f63ea1 --- /dev/null +++ b/src/Ocelot/Requester/QoS/IQoSProviderFactory.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Configuration; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Logging; +using Ocelot.Responses; +using Polly; +using Polly.CircuitBreaker; +using Polly.Timeout; + +namespace Ocelot.Requester.QoS +{ + public interface IQoSProviderFactory + { + IQoSProvider Get(ReRoute reRoute); + } + + public class QoSProviderFactory : IQoSProviderFactory + { + private readonly IOcelotLoggerFactory _loggerFactory; + + public QoSProviderFactory(IOcelotLoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + } + + public IQoSProvider Get(ReRoute reRoute) + { + if (reRoute.IsQos) + { + return new PollyQoSProvider(reRoute, _loggerFactory); + } + + return new NoQoSProvider(); + } + } + + public interface IQoSProvider + { + CircuitBreaker CircuitBreaker { get; } + } + + public class NoQoSProvider : IQoSProvider + { + public CircuitBreaker CircuitBreaker { get; } + } + + public class PollyQoSProvider : IQoSProvider + { + private readonly CircuitBreakerPolicy _circuitBreakerPolicy; + private readonly TimeoutPolicy _timeoutPolicy; + private readonly IOcelotLogger _logger; + private readonly CircuitBreaker _circuitBreaker; + + public PollyQoSProvider(ReRoute reRoute, IOcelotLoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + + _timeoutPolicy = Policy.TimeoutAsync(reRoute.QosOptions.TimeoutValue, reRoute.QosOptions.TimeoutStrategy); + + _circuitBreakerPolicy = Policy + .Handle() + .Or() + .Or() + .CircuitBreakerAsync( + exceptionsAllowedBeforeBreaking: reRoute.QosOptions.ExceptionsAllowedBeforeBreaking, + durationOfBreak: reRoute.QosOptions.DurationOfBreak, + onBreak: (ex, breakDelay) => + { + _logger.LogError( + ".Breaker logging: Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms!", ex); + }, + onReset: () => + { + _logger.LogDebug(".Breaker logging: Call ok! Closed the circuit again."); + }, + onHalfOpen: () => + { + _logger.LogDebug(".Breaker logging: Half-open; next call is a trial."); + } + ); + + _circuitBreaker = new CircuitBreaker(_circuitBreakerPolicy, _timeoutPolicy); + } + + public CircuitBreaker CircuitBreaker => _circuitBreaker; + } + + public class CircuitBreaker + { + public CircuitBreaker(CircuitBreakerPolicy circuitBreakerPolicy, TimeoutPolicy timeoutPolicy) + { + CircuitBreakerPolicy = circuitBreakerPolicy; + TimeoutPolicy = timeoutPolicy; + } + + public CircuitBreakerPolicy CircuitBreakerPolicy { get; private set; } + public TimeoutPolicy TimeoutPolicy { get; private set; } + } + + + public interface IQosProviderHouse + { + Response Get(string key); + Response Add(string key, IQoSProvider loadBalancer); + } + + public class QosProviderHouse : IQosProviderHouse + { + private readonly Dictionary _qoSProviders; + + public QosProviderHouse() + { + _qoSProviders = new Dictionary(); + } + + public Response Get(string key) + { + IQoSProvider qoSProvider; + + if (_qoSProviders.TryGetValue(key, out qoSProvider)) + { + return new OkResponse(_qoSProviders[key]); + } + + return new ErrorResponse(new List() + { + new UnableToFindQoSProviderError($"unabe to find qos provider for {key}") + }); + } + + public Response Add(string key, IQoSProvider loadBalancer) + { + if (!_qoSProviders.ContainsKey(key)) + { + _qoSProviders.Add(key, loadBalancer); + } + + _qoSProviders.Remove(key); + _qoSProviders.Add(key, loadBalancer); + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs b/src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs new file mode 100644 index 00000000..46ec65f3 --- /dev/null +++ b/src/Ocelot/Requester/QoS/UnableToFindQoSProviderError.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Errors; + +namespace Ocelot.Requester.QoS +{ + public class UnableToFindQoSProviderError : Error + { + public UnableToFindQoSProviderError(string message) + : base(message, OcelotErrorCode.UnableToFindQoSProviderError) + { + } + } +} diff --git a/src/Ocelot/Requester/RequestTimedOutError.cs b/src/Ocelot/Requester/RequestTimedOutError.cs new file mode 100644 index 00000000..38afce1a --- /dev/null +++ b/src/Ocelot/Requester/RequestTimedOutError.cs @@ -0,0 +1,13 @@ +using System; +using Ocelot.Errors; + +namespace Ocelot.Requester +{ + public class RequestTimedOutError : Error + { + public RequestTimedOutError(Exception exception) + : base($"Timeout making http request, exception: {exception.Message}", OcelotErrorCode.RequestTimedOutError) + { + } + } +} diff --git a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs index 6ecc20eb..45aeafd1 100644 --- a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs +++ b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs @@ -22,6 +22,11 @@ namespace Ocelot.Responder return new OkResponse(403); } + if (errors.Any(e => e.Code == OcelotErrorCode.RequestTimedOutError)) + { + return new OkResponse(503); + } + return new OkResponse(404); } } diff --git a/src/Ocelot/Responder/HttpContextResponder.cs b/src/Ocelot/Responder/HttpContextResponder.cs index 8b61e13b..20313e8f 100644 --- a/src/Ocelot/Responder/HttpContextResponder.cs +++ b/src/Ocelot/Responder/HttpContextResponder.cs @@ -1,5 +1,6 @@ using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -24,7 +25,7 @@ namespace Ocelot.Responder _removeOutputHeaders = removeOutputHeaders; } - public async Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response) + public async Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response) { _removeOutputHeaders.Remove(response.Headers); @@ -54,9 +55,11 @@ namespace Ocelot.Responder using (Stream stream = new MemoryStream(content)) { - await stream.CopyToAsync(context.Response.Body); + if (response.StatusCode != HttpStatusCode.NotModified) + { + await stream.CopyToAsync(context.Response.Body); + } } - return new OkResponse(); } private static void AddHeaderIfDoesntExist(HttpContext context, KeyValuePair> httpResponseHeader) @@ -67,14 +70,13 @@ namespace Ocelot.Responder } } - public async Task SetErrorResponseOnContext(HttpContext context, int statusCode) + public void SetErrorResponseOnContext(HttpContext context, int statusCode) { context.Response.OnStarting(x => { context.Response.StatusCode = statusCode; return Task.CompletedTask; }, context); - return new OkResponse(); } } } \ No newline at end of file diff --git a/src/Ocelot/Responder/IHttpResponder.cs b/src/Ocelot/Responder/IHttpResponder.cs index 5292f4df..f885c673 100644 --- a/src/Ocelot/Responder/IHttpResponder.cs +++ b/src/Ocelot/Responder/IHttpResponder.cs @@ -1,13 +1,12 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Ocelot.Responses; namespace Ocelot.Responder { public interface IHttpResponder { - Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response); - Task SetErrorResponseOnContext(HttpContext context, int statusCode); + Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response); + void SetErrorResponseOnContext(HttpContext context, int statusCode); } } diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs index 06da92dc..6bce4ac6 100644 --- a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs +++ b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs @@ -46,34 +46,27 @@ namespace Ocelot.Responder.Middleware _logger.LogDebug("received errors setting error response"); - await SetErrorResponse(context, errors); + SetErrorResponse(context, errors); } else { _logger.LogDebug("no pipeline error, setting response"); - var setResponse = await _responder.SetResponseOnHttpContext(context, HttpResponseMessage); - - if (setResponse.IsError) - { - _logger.LogDebug("error setting response, returning error to client"); - - await SetErrorResponse(context, setResponse.Errors); - } + await _responder.SetResponseOnHttpContext(context, HttpResponseMessage); } } - private async Task SetErrorResponse(HttpContext context, List errors) + private void SetErrorResponse(HttpContext context, List errors) { var statusCode = _codeMapper.Map(errors); if (!statusCode.IsError) { - await _responder.SetErrorResponseOnContext(context, statusCode.Data); + _responder.SetErrorResponseOnContext(context, statusCode.Data); } else { - await _responder.SetErrorResponseOnContext(context, 500); + _responder.SetErrorResponseOnContext(context, 500); } } } diff --git a/src/Ocelot/Responses/ErrorResponse.cs b/src/Ocelot/Responses/ErrorResponse.cs index 9f541507..bf20dbd1 100644 --- a/src/Ocelot/Responses/ErrorResponse.cs +++ b/src/Ocelot/Responses/ErrorResponse.cs @@ -5,6 +5,9 @@ namespace Ocelot.Responses { public class ErrorResponse : Response { + public ErrorResponse(Error error) : base(new List{error}) + { + } public ErrorResponse(List errors) : base(errors) { } diff --git a/src/Ocelot/Responses/ErrorResponseGeneric.cs b/src/Ocelot/Responses/ErrorResponseGeneric.cs index 77230503..044d6f61 100644 --- a/src/Ocelot/Responses/ErrorResponseGeneric.cs +++ b/src/Ocelot/Responses/ErrorResponseGeneric.cs @@ -5,7 +5,13 @@ namespace Ocelot.Responses { public class ErrorResponse : Response { - public ErrorResponse(List errors) : base(errors) + public ErrorResponse(Error error) + : base(new List {error}) + { + + } + public ErrorResponse(List errors) + : base(errors) { } } diff --git a/src/Ocelot/ServiceDiscovery/ConfigurationServiceProvider.cs b/src/Ocelot/ServiceDiscovery/ConfigurationServiceProvider.cs new file mode 100644 index 00000000..98e7b0f6 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/ConfigurationServiceProvider.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery +{ + public class ConfigurationServiceProvider : IServiceDiscoveryProvider + { + private readonly List _services; + + public ConfigurationServiceProvider(List services) + { + _services = services; + } + + public async Task> Get() + { + return await Task.FromResult(_services); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/ConsulRegistryConfiguration.cs b/src/Ocelot/ServiceDiscovery/ConsulRegistryConfiguration.cs new file mode 100644 index 00000000..8d496a85 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/ConsulRegistryConfiguration.cs @@ -0,0 +1,16 @@ +namespace Ocelot.ServiceDiscovery +{ + public class ConsulRegistryConfiguration + { + public ConsulRegistryConfiguration(string hostName, int port, string serviceName) + { + HostName = hostName; + Port = port; + ServiceName = serviceName; + } + + public string ServiceName { get; private set; } + public string HostName { get; private set; } + public int Port { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs new file mode 100644 index 00000000..c74c90f0 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Consul; +using Ocelot.Infrastructure.Extensions; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery +{ + public class ConsulServiceDiscoveryProvider : IServiceDiscoveryProvider + { + private readonly ConsulRegistryConfiguration _configuration; + private readonly ConsulClient _consul; + private const string VersionPrefix = "version-"; + + public ConsulServiceDiscoveryProvider(ConsulRegistryConfiguration consulRegistryConfiguration) + { + var consulHost = string.IsNullOrEmpty(consulRegistryConfiguration?.HostName) ? "localhost" : consulRegistryConfiguration.HostName; + var consulPort = consulRegistryConfiguration?.Port ?? 8500; + _configuration = new ConsulRegistryConfiguration(consulHost, consulPort, consulRegistryConfiguration?.ServiceName); + + _consul = new ConsulClient(config => + { + config.Address = new Uri($"http://{_configuration.HostName}:{_configuration.Port}"); + }); + } + + public async Task> Get() + { + var queryResult = await _consul.Health.Service(_configuration.ServiceName, string.Empty, true); + + var services = queryResult.Response.Select(BuildService); + + return services.ToList(); + } + + private Service BuildService(ServiceEntry serviceEntry) + { + return new Service( + serviceEntry.Service.Service, + new HostAndPort(serviceEntry.Service.Address, serviceEntry.Service.Port), + serviceEntry.Service.ID, + GetVersionFromStrings(serviceEntry.Service.Tags), + serviceEntry.Service.Tags ?? Enumerable.Empty()); + } + + private string GetVersionFromStrings(IEnumerable strings) + { + return strings + ?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) + .TrimStart(VersionPrefix); + } + } +} diff --git a/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProvider.cs new file mode 100644 index 00000000..2c643d4b --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProvider.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery +{ + public interface IServiceDiscoveryProvider + { + Task> Get(); + } +} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs new file mode 100644 index 00000000..6c6c3d4c --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs @@ -0,0 +1,10 @@ +using System; +using Ocelot.Configuration; + +namespace Ocelot.ServiceDiscovery +{ + public interface IServiceDiscoveryProviderFactory + { + IServiceDiscoveryProvider Get(ServiceProviderConfiguraion serviceConfig); + } +} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs new file mode 100644 index 00000000..00622190 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using Ocelot.Configuration; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery +{ + public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory + { + public IServiceDiscoveryProvider Get(ServiceProviderConfiguraion serviceConfig) + { + if (serviceConfig.UseServiceDiscovery) + { + return GetServiceDiscoveryProvider(serviceConfig.ServiceName, serviceConfig.ServiceDiscoveryProvider, serviceConfig.ServiceProviderHost, serviceConfig.ServiceProviderPort); + } + + var services = new List() + { + new Service(serviceConfig.ServiceName, + new HostAndPort(serviceConfig.DownstreamHost, serviceConfig.DownstreamPort), + string.Empty, + string.Empty, + new string[0]) + }; + + return new ConfigurationServiceProvider(services); + } + + private IServiceDiscoveryProvider GetServiceDiscoveryProvider(string serviceName, string serviceProviderName, string providerHostName, int providerPort) + { + var consulRegistryConfiguration = new ConsulRegistryConfiguration(providerHostName, providerPort, serviceName); + return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs b/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs new file mode 100644 index 00000000..163e63ef --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.ServiceDiscovery +{ + public class UnableToFindServiceDiscoveryProviderError : Error + { + public UnableToFindServiceDiscoveryProviderError(string message) + : base(message, OcelotErrorCode.UnableToFindServiceDiscoveryProviderError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Values/DownstreamPath.cs b/src/Ocelot/Values/DownstreamPath.cs new file mode 100644 index 00000000..90f2e83e --- /dev/null +++ b/src/Ocelot/Values/DownstreamPath.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Values +{ + public class DownstreamPath + { + public DownstreamPath(string value) + { + Value = value; + } + + public string Value { get; private set; } + } +} diff --git a/src/Ocelot/Values/DownstreamPathTemplate.cs b/src/Ocelot/Values/DownstreamPathTemplate.cs new file mode 100644 index 00000000..b0b53418 --- /dev/null +++ b/src/Ocelot/Values/DownstreamPathTemplate.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Values +{ + public class PathTemplate + { + public PathTemplate(string value) + { + Value = value; + } + + public string Value { get; private set; } + } +} diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrl.cs b/src/Ocelot/Values/DownstreamUrl.cs similarity index 74% rename from src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrl.cs rename to src/Ocelot/Values/DownstreamUrl.cs index ea90179e..f809c84b 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrl.cs +++ b/src/Ocelot/Values/DownstreamUrl.cs @@ -1,4 +1,4 @@ -namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer +namespace Ocelot.Values { public class DownstreamUrl { @@ -9,4 +9,4 @@ public string Value { get; private set; } } -} +} \ No newline at end of file diff --git a/src/Ocelot/Values/HostAndPort.cs b/src/Ocelot/Values/HostAndPort.cs new file mode 100644 index 00000000..c1219527 --- /dev/null +++ b/src/Ocelot/Values/HostAndPort.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Values +{ + public class HostAndPort + { + public HostAndPort(string downstreamHost, int downstreamPort) + { + DownstreamHost = downstreamHost?.Trim('/'); + DownstreamPort = downstreamPort; + } + + public string DownstreamHost { get; private set; } + public int DownstreamPort { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Values/Service.cs b/src/Ocelot/Values/Service.cs new file mode 100644 index 00000000..0ba12b79 --- /dev/null +++ b/src/Ocelot/Values/Service.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Ocelot.Values +{ + public class Service + { + public Service(string name, + HostAndPort hostAndPort, + string id, + string version, + IEnumerable tags) + { + Name = name; + HostAndPort = hostAndPort; + Id = id; + Version = version; + Tags = tags; + } + public string Id { get; private set; } + + public string Name { get; private set; } + + public string Version { get; private set; } + + public IEnumerable Tags { get; private set; } + + public HostAndPort HostAndPort { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/project.json b/src/Ocelot/project.json index e5f5389c..93484a9d 100644 --- a/src/Ocelot/project.json +++ b/src/Ocelot/project.json @@ -1,42 +1,55 @@ { - "version": "1.0.0-*", - - "dependencies": { - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0", - "Microsoft.Extensions.Configuration.Json": "1.0.0", - "Microsoft.Extensions.Logging": "1.0.0", - "Microsoft.Extensions.Logging.Console": "1.0.0", - "Microsoft.Extensions.Logging.Debug": "1.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", - "Microsoft.AspNetCore.Http": "1.0.0", - "System.Text.RegularExpressions": "4.1.0", - "Microsoft.AspNetCore.Authentication.OAuth": "1.0.0", - "Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0", - "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0", - "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0", - "Microsoft.AspNetCore.Authentication.Google": "1.0.0", - "Microsoft.AspNetCore.Authentication.Facebook": "1.0.0", - "Microsoft.AspNetCore.Authentication.Twitter": "1.0.0", - "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.0.0", - "Microsoft.AspNetCore.Authentication": "1.0.0", - "IdentityServer4.AccessTokenValidation": "1.0.1-rc2", - "Microsoft.AspNetCore.Mvc": "1.0.1", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", - "Microsoft.NETCore.App": { - "version": "1.0.1", - "type": "platform" - }, - "CacheManager.Core": "0.9.1", - "CacheManager.Microsoft.Extensions.Configuration": "0.9.1", - "CacheManager.Microsoft.Extensions.Logging": "0.9.1" - }, - - "frameworks": { - "netcoreapp1.4": { - "imports": [ - ] - } + "version": "0.0.0-dev", + "title": "Ocelot", + "summary": "API Gateway created using .NET core.", + "projectUrl": "https://github.com/TomPallister/Ocelot", + "description": "This project is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. In particular I want easy integration with IdentityServer reference and bearer tokens. We have been unable to find this in my current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens. We would rather use the IdentityServer code that already exists to do this. Ocelot is a bunch of middlewares in a specific order. Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is stored in a per request scoped repository and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features.", + "tags": [ + "API Gateway", + ".NET core" + ], + "dependencies": { + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "System.Text.RegularExpressions": "4.3.0", + "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", + "Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0", + "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0", + "Microsoft.AspNetCore.Authentication.Cookies": "1.1.0", + "Microsoft.AspNetCore.Authentication.Google": "1.1.0", + "Microsoft.AspNetCore.Authentication.Facebook": "1.1.0", + "Microsoft.AspNetCore.Authentication.Twitter": "1.1.0", + "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.1.0", + "Microsoft.AspNetCore.Authentication": "1.1.0", + "IdentityServer4.AccessTokenValidation": "1.0.2", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "CacheManager.Core": "0.9.2", + "CacheManager.Microsoft.Extensions.Configuration": "0.9.2", + "CacheManager.Microsoft.Extensions.Logging": "0.9.2", + "Consul": "0.7.2.1", + "Polly": "5.0.3", + "IdentityServer4": "1.0.1", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0" + }, + "runtimes": { + "win10-x64": {}, + "osx.10.11-x64": {}, + "osx.10.12-x64": {}, + "win7-x64": {} + }, + "frameworks": { + "netcoreapp1.1": { + "imports": [ + ] } + } } diff --git a/src/Ocelot/project.json.orig b/src/Ocelot/project.json.orig new file mode 100644 index 00000000..bac33d54 --- /dev/null +++ b/src/Ocelot/project.json.orig @@ -0,0 +1,88 @@ +{ + "version": "0.0.0-dev", +<<<<<<< HEAD + "dependencies": { + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "System.Text.RegularExpressions": "4.3.0", + "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", + "Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0", + "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0", + "Microsoft.AspNetCore.Authentication.Cookies": "1.1.0", + "Microsoft.AspNetCore.Authentication.Google": "1.1.0", + "Microsoft.AspNetCore.Authentication.Facebook": "1.1.0", + "Microsoft.AspNetCore.Authentication.Twitter": "1.1.0", + "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.1.0", + "Microsoft.AspNetCore.Authentication": "1.1.0", + "IdentityServer4.AccessTokenValidation": "1.0.2", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "CacheManager.Core": "0.9.2", + "CacheManager.Microsoft.Extensions.Configuration": "0.9.2", + "CacheManager.Microsoft.Extensions.Logging": "0.9.2", + "Consul": "0.7.2.1", + "Polly": "5.0.3", + "IdentityServer4": "1.0.1", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0" + }, +======= + "title": "Ocelot", + "summary": "API Gateway created using .NET core.", + "projectUrl": "https://github.com/TomPallister/Ocelot", + "description": "This project is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. In particular I want easy integration with IdentityServer reference and bearer tokens. We have been unable to find this in my current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens. We would rather use the IdentityServer code that already exists to do this. Ocelot is a bunch of middlewares in a specific order. Ocelot manipulates the HttpRequest object into a state specified by its configuration until it reaches a request builder middleware where it creates a HttpRequestMessage object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is stored in a per request scoped repository and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. That is basically it with a bunch of other features.", + "tags": [ + "API Gateway", + ".NET core" + ], + "dependencies": { + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "System.Text.RegularExpressions": "4.3.0", + "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", + "Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0", + "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0", + "Microsoft.AspNetCore.Authentication.Cookies": "1.1.0", + "Microsoft.AspNetCore.Authentication.Google": "1.1.0", + "Microsoft.AspNetCore.Authentication.Facebook": "1.1.0", + "Microsoft.AspNetCore.Authentication.Twitter": "1.1.0", + "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.1.0", + "Microsoft.AspNetCore.Authentication": "1.1.0", + "IdentityServer4.AccessTokenValidation": "1.0.2", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "CacheManager.Core": "0.9.2", + "CacheManager.Microsoft.Extensions.Configuration": "0.9.2", + "CacheManager.Microsoft.Extensions.Logging": "0.9.2", + "Consul": "0.7.2.1", + "Polly": "5.0.3" + }, +>>>>>>> develop + "runtimes": { + "win10-x64": {}, + "osx.10.11-x64": {}, + "osx.10.12-x64": {}, + "win7-x64": {} + }, + "frameworks": { + "netcoreapp1.1": { + "imports": [ + ] + } + } +} diff --git a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs index da63d134..6dca1dc2 100644 --- a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs @@ -4,7 +4,6 @@ using System.IO; using System.Net; using System.Security.Claims; using IdentityServer4.Models; -using IdentityServer4.Services.InMemory; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -15,13 +14,20 @@ using Xunit; namespace Ocelot.AcceptanceTests { + using IdentityServer4; + using IdentityServer4.Test; + public class AuthenticationTests : IDisposable { private IWebHost _servicebuilder; private readonly Steps _steps; private IWebHost _identityServerBuilder; private string _identityServerRootUrl = "http://localhost:51888"; - private string _downstreamServiceRootUrl = "http://localhost:51876/"; + private string _downstreamServicePath = "/"; + private string _downstreamServiceHost = "localhost"; + private int _downstreamServicePort = 51876; + private string _downstreamServiceScheme = "http"; + private string _downstreamServiceUrl = "http://localhost:51876"; public AuthenticationTests() { @@ -37,8 +43,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = _downstreamServiceRootUrl, - UpstreamTemplate = "/", + DownstreamPathTemplate = _downstreamServicePath, + DownstreamPort = _downstreamServicePort, + DownstreamHost = _downstreamServiceHost, + DownstreamScheme = _downstreamServiceScheme, + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Post", AuthenticationOptions = new FileAuthenticationOptions { @@ -54,7 +63,7 @@ namespace Ocelot.AcceptanceTests }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt)) - .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceRootUrl, 201, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceUrl, 201, string.Empty)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenThePostHasContent("postContent")) @@ -72,8 +81,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = _downstreamServiceRootUrl, - UpstreamTemplate = "/", + DownstreamPathTemplate = _downstreamServicePath, + DownstreamPort = _downstreamServicePort, + DownstreamHost = _downstreamServiceHost, + DownstreamScheme = _downstreamServiceScheme, + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Post", AuthenticationOptions = new FileAuthenticationOptions { @@ -89,7 +101,7 @@ namespace Ocelot.AcceptanceTests }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Reference)) - .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceRootUrl, 201, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceUrl, 201, string.Empty)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.GivenThePostHasContent("postContent")) @@ -107,8 +119,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = _downstreamServiceRootUrl, - UpstreamTemplate = "/", + DownstreamPathTemplate = _downstreamServicePath, + DownstreamPort = _downstreamServicePort, + DownstreamHost = _downstreamServiceHost, + DownstreamScheme = _downstreamServiceScheme, + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", AuthenticationOptions = new FileAuthenticationOptions { @@ -124,7 +139,7 @@ namespace Ocelot.AcceptanceTests }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt)) - .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceRootUrl, 200, "Hello from Laura")) + .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceUrl, 200, "Hello from Laura")) .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) @@ -144,9 +159,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = _downstreamServiceRootUrl, - UpstreamTemplate = "/", + DownstreamPathTemplate = _downstreamServicePath, + DownstreamPort = _downstreamServicePort, + DownstreamHost = _downstreamServiceHost, + DownstreamScheme = _downstreamServiceScheme, + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Post", + AuthenticationOptions = new FileAuthenticationOptions { AdditionalScopes = new List(), @@ -161,7 +180,7 @@ namespace Ocelot.AcceptanceTests }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt)) - .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceRootUrl, 201, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceUrl, 201, string.Empty)) .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) @@ -181,10 +200,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = _downstreamServiceRootUrl, - UpstreamTemplate = "/", + DownstreamPathTemplate = _downstreamServicePath, + DownstreamPort = _downstreamServicePort, + DownstreamHost = _downstreamServiceHost, + DownstreamScheme = _downstreamServiceScheme, + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Post", - AuthenticationOptions = new FileAuthenticationOptions + AuthenticationOptions = new FileAuthenticationOptions { AdditionalScopes = new List(), Provider = "IdentityServer", @@ -198,7 +220,7 @@ namespace Ocelot.AcceptanceTests }; this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Reference)) - .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceRootUrl, 201, string.Empty)) + .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceUrl, 201, string.Empty)) .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) @@ -241,26 +263,34 @@ namespace Ocelot.AcceptanceTests .ConfigureServices(services => { services.AddLogging(); - services.AddDeveloperIdentityServer() - .AddInMemoryScopes(new List + services.AddIdentityServer() + .AddTemporarySigningCredential() + .AddInMemoryApiResources(new List { - new Scope + new ApiResource { Name = scopeName, Description = "My API", Enabled = true, - AllowUnrestrictedIntrospection = true, - ScopeSecrets = new List() + DisplayName = "test", + Scopes = new List() + { + new Scope("api"), + new Scope("openid"), + new Scope("offline_access") + }, + ApiSecrets = new List() { new Secret { Value = "secret".Sha256() } + }, + UserClaims = new List() + { + "CustomerId", "LocationId" } }, - - StandardScopes.OpenId, - StandardScopes.OfflineAccess }) .AddInMemoryClients(new List { @@ -275,14 +305,13 @@ namespace Ocelot.AcceptanceTests RequireClientSecret = false } }) - .AddInMemoryUsers(new List + .AddTestUsers(new List { - new InMemoryUser + new TestUser { Username = "test", Password = "test", - Enabled = true, - Subject = "registered|1231231", + SubjectId = "registered|1231231", Claims = new List { new Claim("CustomerId", "123"), diff --git a/test/Ocelot.AcceptanceTests/AuthorisationTests.cs b/test/Ocelot.AcceptanceTests/AuthorisationTests.cs index 8dfb1314..a89616b9 100644 --- a/test/Ocelot.AcceptanceTests/AuthorisationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthorisationTests.cs @@ -4,7 +4,6 @@ using System.IO; using System.Net; using System.Security.Claims; using IdentityServer4.Models; -using IdentityServer4.Services.InMemory; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -15,6 +14,9 @@ using Xunit; namespace Ocelot.AcceptanceTests { + using IdentityServer4; + using IdentityServer4.Test; + public class AuthorisationTests : IDisposable { private IWebHost _servicebuilder; @@ -35,8 +37,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51876/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 51876, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", AuthenticationOptions = new FileAuthenticationOptions { @@ -89,8 +94,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51876/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 51876, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", AuthenticationOptions = new FileAuthenticationOptions { @@ -164,27 +172,35 @@ namespace Ocelot.AcceptanceTests .ConfigureServices(services => { services.AddLogging(); - services.AddDeveloperIdentityServer() - .AddInMemoryScopes(new List + services.AddIdentityServer() + .AddTemporarySigningCredential() + .AddInMemoryApiResources(new List { - new Scope + new ApiResource { Name = scopeName, Description = "My API", Enabled = true, - AllowUnrestrictedIntrospection = true, - ScopeSecrets = new List() + DisplayName = "test", + Scopes = new List() + { + new Scope("api"), + new Scope("openid"), + new Scope("offline_access") + }, + ApiSecrets = new List() { new Secret { Value = "secret".Sha256() } }, - IncludeAllClaimsForUser = true + UserClaims = new List() + { + "CustomerId", "LocationId", "UserType", "UserId" + } }, - StandardScopes.OpenId, - StandardScopes.OfflineAccess }) .AddInMemoryClients(new List { @@ -199,14 +215,13 @@ namespace Ocelot.AcceptanceTests RequireClientSecret = false } }) - .AddInMemoryUsers(new List + .AddTestUsers(new List { - new InMemoryUser + new TestUser { Username = "test", Password = "test", - Enabled = true, - Subject = "registered|1231231", + SubjectId = "registered|1231231", Claims = new List { new Claim("CustomerId", "123"), diff --git a/test/Ocelot.AcceptanceTests/CachingTests.cs b/test/Ocelot.AcceptanceTests/CachingTests.cs index 34f10b7a..a8364855 100644 --- a/test/Ocelot.AcceptanceTests/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/CachingTests.cs @@ -31,8 +31,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", FileCacheOptions = new FileCacheOptions { @@ -64,8 +67,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", FileCacheOptions = new FileCacheOptions { diff --git a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs index 81a12381..85e8fd68 100644 --- a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs @@ -30,9 +30,12 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/api/products/{productId}", - UpstreamTemplate = "/products/{productId}", - UpstreamHttpMethod = "Get" + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", } } }; @@ -54,10 +57,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/api/products/{productId}", - UpstreamTemplate = "/products/{productId}", + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", - ReRouteIsCaseSensitive = false + ReRouteIsCaseSensitive = false, } } }; @@ -79,10 +85,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/api/products/{productId}", - UpstreamTemplate = "/products/{productId}", + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", - ReRouteIsCaseSensitive = true + ReRouteIsCaseSensitive = true, } } }; @@ -104,10 +113,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/api/products/{productId}", - UpstreamTemplate = "/PRODUCTS/{productId}", + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/PRODUCTS/{productId}", UpstreamHttpMethod = "Get", - ReRouteIsCaseSensitive = true + ReRouteIsCaseSensitive = true, } } }; @@ -129,10 +141,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/api/products/{productId}", - UpstreamTemplate = "/products/{productId}", + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", - ReRouteIsCaseSensitive = true + ReRouteIsCaseSensitive = true, } } }; @@ -154,10 +169,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/api/products/{productId}", - UpstreamTemplate = "/PRODUCTS/{productId}", + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/PRODUCTS/{productId}", UpstreamHttpMethod = "Get", - ReRouteIsCaseSensitive = true + ReRouteIsCaseSensitive = true, } } }; diff --git a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs index c181a445..2d6bf163 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net; using System.Security.Claims; using IdentityServer4.Models; -using IdentityServer4.Services.InMemory; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -17,6 +16,9 @@ using Xunit; [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace Ocelot.AcceptanceTests { + using IdentityServer4; + using IdentityServer4.Test; + public class ClaimsToHeadersForwardingTests : IDisposable { private IWebHost _servicebuilder; @@ -31,12 +33,11 @@ namespace Ocelot.AcceptanceTests [Fact] public void should_return_response_200_and_foward_claim_as_header() { - var user = new InMemoryUser + var user = new TestUser() { Username = "test", Password = "test", - Enabled = true, - Subject = "registered|1231231", + SubjectId = "registered|1231231", Claims = new List { new Claim("CustomerId", "123"), @@ -50,8 +51,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:52876/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 52876, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", AuthenticationOptions = new FileAuthenticationOptions { @@ -115,7 +119,7 @@ namespace Ocelot.AcceptanceTests _servicebuilder.Start(); } - private void GivenThereIsAnIdentityServerOn(string url, string scopeName, AccessTokenType tokenType, InMemoryUser user) + private void GivenThereIsAnIdentityServerOn(string url, string scopeName, AccessTokenType tokenType, TestUser user) { _identityServerBuilder = new WebHostBuilder() .UseUrls(url) @@ -126,27 +130,34 @@ namespace Ocelot.AcceptanceTests .ConfigureServices(services => { services.AddLogging(); - services.AddDeveloperIdentityServer() - .AddInMemoryScopes(new List + services.AddIdentityServer() + .AddTemporarySigningCredential() + .AddInMemoryApiResources(new List { - new Scope + new ApiResource { Name = scopeName, Description = "My API", Enabled = true, - AllowUnrestrictedIntrospection = true, - ScopeSecrets = new List() + DisplayName = "test", + Scopes = new List() + { + new Scope("api"), + new Scope("openid"), + new Scope("offline_access") + }, + ApiSecrets = new List() { new Secret { Value = "secret".Sha256() } }, - IncludeAllClaimsForUser = true - }, - - StandardScopes.OpenId, - StandardScopes.OfflineAccess + UserClaims = new List() + { + "CustomerId", "LocationId", "UserType", "UserId" + } + } }) .AddInMemoryClients(new List { @@ -161,7 +172,7 @@ namespace Ocelot.AcceptanceTests RequireClientSecret = false } }) - .AddInMemoryUsers(new List + .AddTestUsers(new List { user }); diff --git a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs index 51e76942..0c10c3e4 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Net; using System.Security.Claims; using IdentityServer4.Models; -using IdentityServer4.Services.InMemory; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -17,6 +16,9 @@ using Xunit; namespace Ocelot.AcceptanceTests { + using IdentityServer4; + using IdentityServer4.Test; + public class ClaimsToQueryStringForwardingTests : IDisposable { private IWebHost _servicebuilder; @@ -31,12 +33,11 @@ namespace Ocelot.AcceptanceTests [Fact] public void should_return_response_200_and_foward_claim_as_query_string() { - var user = new InMemoryUser + var user = new TestUser() { Username = "test", Password = "test", - Enabled = true, - Subject = "registered|1231231", + SubjectId = "registered|1231231", Claims = new List { new Claim("CustomerId", "123"), @@ -50,8 +51,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:57876/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 57876, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", AuthenticationOptions = new FileAuthenticationOptions { @@ -122,7 +126,7 @@ namespace Ocelot.AcceptanceTests _servicebuilder.Start(); } - private void GivenThereIsAnIdentityServerOn(string url, string scopeName, AccessTokenType tokenType, InMemoryUser user) + private void GivenThereIsAnIdentityServerOn(string url, string scopeName, AccessTokenType tokenType, TestUser user) { _identityServerBuilder = new WebHostBuilder() .UseUrls(url) @@ -133,27 +137,34 @@ namespace Ocelot.AcceptanceTests .ConfigureServices(services => { services.AddLogging(); - services.AddDeveloperIdentityServer() - .AddInMemoryScopes(new List + services.AddIdentityServer() + .AddTemporarySigningCredential() + .AddInMemoryApiResources(new List { - new Scope + new ApiResource { - Name = scopeName, + Name = scopeName, Description = "My API", Enabled = true, - AllowUnrestrictedIntrospection = true, - ScopeSecrets = new List() + DisplayName = "test", + Scopes = new List() + { + new Scope("api"), + new Scope("openid"), + new Scope("offline_access") + }, + ApiSecrets = new List() { new Secret { Value = "secret".Sha256() } }, - IncludeAllClaimsForUser = true - }, - - StandardScopes.OpenId, - StandardScopes.OfflineAccess + UserClaims = new List() + { + "CustomerId", "LocationId", "UserType", "UserId" + } + } }) .AddInMemoryClients(new List { @@ -168,7 +179,7 @@ namespace Ocelot.AcceptanceTests RequireClientSecret = false } }) - .AddInMemoryUsers(new List + .AddTestUsers(new List { user }); diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs new file mode 100644 index 00000000..959777c6 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs @@ -0,0 +1,165 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ClientRateLimitTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + private int _counterOne; + + + public ClientRateLimitTests() + { + _steps = new Steps(); + } + + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + + [Fact] + public void should_call_withratelimiting() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = "Get", + RequestIdKey = _steps.RequestIdKey, + + RateLimitOptions = new FileRateLimitRule() + { + EnableRateLimiting = true, + ClientWhitelist = new List(), + Limit = 3, + Period = "1s", + PeriodTimespan = 1000 + } + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + RateLimitOptions = new FileRateLimitOptions() + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = "", + RateLimitCounterPrefix = "", + HttpStatusCode = 428 + + }, + RequestIdKey ="oceclientrequest" + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/ClientRateLimit")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 2)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit",1)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(428)) + .BDDfy(); + } + + + [Fact] + public void should_call_middleware_withWhitelistClient() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/ClientRateLimit", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/api/ClientRateLimit", + UpstreamHttpMethod = "Get", + RequestIdKey = _steps.RequestIdKey, + + RateLimitOptions = new FileRateLimitRule() + { + EnableRateLimiting = true, + ClientWhitelist = new List() { "ocelotclient1"}, + Limit = 3, + Period = "1s", + PeriodTimespan = 100 + } + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + RateLimitOptions = new FileRateLimitOptions() + { + ClientIdHeader = "ClientId", + DisableRateLimitHeaders = false, + QuotaExceededMessage = "", + RateLimitCounterPrefix = "" + }, + RequestIdKey = "oceclientrequest" + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/ClientRateLimit")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit("/api/ClientRateLimit", 4)) + .Then(x => _steps.ThenTheStatusCodeShouldBe(200)) + .BDDfy(); + } + + + private void GivenThereIsAServiceRunningOn(string url) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(context => + { + _counterOne++; + context.Response.StatusCode = 200; + context.Response.WriteAsync(_counterOne.ToString()); + return Task.CompletedTask; + }); + }) + .Build(); + + _builder.Start(); + } + + + } +} \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs index 1a267a98..7d33febb 100644 --- a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs +++ b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs @@ -45,8 +45,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:41879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", } } @@ -79,9 +82,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:41879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", + } } }; @@ -113,9 +120,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:41879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "41879/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", + } } }; @@ -147,8 +158,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:41879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", } } @@ -181,8 +195,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:41879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", } } @@ -215,8 +232,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:41879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", } } diff --git a/test/Ocelot.AcceptanceTests/QoSTests.cs b/test/Ocelot.AcceptanceTests/QoSTests.cs new file mode 100644 index 00000000..0eb3d8a4 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/QoSTests.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +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 QoSTests : IDisposable + { + private IWebHost _brokenService; + private readonly Steps _steps; + private int _requestCount; + private IWebHost _workingService; + + public QoSTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_open_circuit_breaker_then_close() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = "Get", + QoSOptions = new FileQoSOptions + { + ExceptionsAllowedBeforeBreaking = 1, + TimeoutValue = 500, + DurationOfBreak = 1000 + } + } + } + }; + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51879", "Hello from Laura")) + .Given(x => _steps.GivenThereIsAConfiguration(configuration)) + .Given(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => x.GivenIWaitMilliseconds(3000)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void open_circuit_should_not_effect_different_reRoute() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = "Get", + QoSOptions = new FileQoSOptions + { + ExceptionsAllowedBeforeBreaking = 1, + TimeoutValue = 500, + DurationOfBreak = 1000 + } + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51880, + UpstreamPathTemplate = "working", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51879", "Hello from Laura")) + .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", 200, "Hello from Tom")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => x.GivenIWaitMilliseconds(3000)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private void GivenIWaitMilliseconds(int ms) + { + Thread.Sleep(ms); + } + + private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody) + { + _brokenService = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + //circuit starts closed + if (_requestCount == 0) + { + _requestCount++; + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); + return; + } + + //request one times out and polly throws exception, circuit opens + if (_requestCount == 1) + { + _requestCount++; + await Task.Delay(1000); + context.Response.StatusCode = 200; + return; + } + + //after break closes we return 200 OK + if (_requestCount == 2) + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); + return; + } + }); + }) + .Build(); + + _brokenService.Start(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + { + _workingService = 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(); + + _workingService.Start(); + } + + public void Dispose() + { + _workingService?.Dispose(); + _brokenService?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/RequestIdTests.cs b/test/Ocelot.AcceptanceTests/RequestIdTests.cs index 512736ae..46a5fc13 100644 --- a/test/Ocelot.AcceptanceTests/RequestIdTests.cs +++ b/test/Ocelot.AcceptanceTests/RequestIdTests.cs @@ -33,11 +33,14 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", - RequestIdKey = _steps.RequestIdKey - } + RequestIdKey = _steps.RequestIdKey, + } } }; @@ -58,10 +61,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", - RequestIdKey = _steps.RequestIdKey + } } }; @@ -85,8 +91,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", } }, diff --git a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs index 902de662..868cb39f 100644 --- a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs +++ b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs @@ -21,7 +21,7 @@ namespace Ocelot.AcceptanceTests } [Fact] - public void should_return_response_200_and_foward_claim_as_header() + public void should_return_internal_server_error_if_downstream_service_returns_internal_server_error() { var configuration = new FileConfiguration { @@ -29,9 +29,12 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:53876/", - UpstreamTemplate = "/", - UpstreamHttpMethod = "Get" + DownstreamPathTemplate = "/", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = "Get", + DownstreamPort = 53876, + DownstreamScheme = "http", + DownstreamHost = "localhost" } } }; diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 84e00d5b..c1a6716c 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -30,7 +30,7 @@ namespace Ocelot.AcceptanceTests .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } - + [Fact] public void should_return_response_200_with_simple_url() { @@ -40,9 +40,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/", - UpstreamTemplate = "/", + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", UpstreamHttpMethod = "Get", + } } }; @@ -56,6 +60,64 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_return_response_200_when_path_missing_forward_slash_as_first_char() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "api/products", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = "Get", + + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products", 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_when_host_has_trailing_slash() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products", + DownstreamScheme = "http", + DownstreamHost = "localhost/", + DownstreamPort = 51879, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = "Get", + + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products", 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_not_care_about_no_trailing() { @@ -65,9 +127,13 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/products", - UpstreamTemplate = "/products/", + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/products/", UpstreamHttpMethod = "Get", + } } }; @@ -90,8 +156,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/products", - UpstreamTemplate = "/products", + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/products", UpstreamHttpMethod = "Get", } } @@ -112,14 +181,23 @@ namespace Ocelot.AcceptanceTests var configuration = new FileConfiguration { ReRoutes = new List + { + new FileReRoute { - new FileReRoute + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + QoSOptions = new FileQoSOptions() { - DownstreamTemplate = "http://localhost:51879/products", - UpstreamTemplate = "/products/{productId}", - UpstreamHttpMethod = "Get", + ExceptionsAllowedBeforeBreaking = 3, + DurationOfBreak = 5, + TimeoutValue = 5000 } } + } }; this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/products", 200, "Hello from Laura")) @@ -139,9 +217,12 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/api/products/{productId}", - UpstreamTemplate = "/products/{productId}", - UpstreamHttpMethod = "Get" + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", } } }; @@ -164,9 +245,12 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/", - UpstreamTemplate = "/", - UpstreamHttpMethod = "Post" + DownstreamPathTemplate = "/", + DownstreamHost = "localhost", + DownstreamPort = 51879, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = "Post", } } }; @@ -189,8 +273,11 @@ namespace Ocelot.AcceptanceTests { new FileReRoute { - DownstreamTemplate = "http://localhost:51879/newThing", - UpstreamTemplate = "/newThing", + DownstreamPathTemplate = "/newThing", + UpstreamPathTemplate = "/newThing", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, UpstreamHttpMethod = "Get", } } diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs new file mode 100644 index 00000000..fcf75b14 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Consul; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ServiceDiscoveryTests : IDisposable + { + private IWebHost _builderOne; + private IWebHost _builderTwo; + private IWebHost _fakeConsulBuilder; + private readonly Steps _steps; + private readonly List _serviceEntries; + private int _counterOne; + private int _counterTwo; + private static readonly object _syncLock = new object(); + + public ServiceDiscoveryTests() + { + _steps = new Steps(); + _serviceEntries = new List(); + } + + [Fact] + public void should_use_service_discovery_and_load_balance_request() + { + var serviceName = "product"; + var downstreamServiceOneUrl = "http://localhost:50879"; + var downstreamServiceTwoUrl = "http://localhost:50880"; + var fakeConsulServiceDiscoveryUrl = "http://localhost:8500"; + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = serviceName, + Address = "localhost", + Port = 50879, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + var serviceEntryTwo = new ServiceEntry() + { + Service = new AgentService() + { + Service = serviceName, + Address = "localhost", + Port = 50880, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = "Get", + ServiceName = serviceName, + LoadBalancer = "LeastConnection", + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() + { + Provider = "Consul", + Host = "localhost", + Port = 8500 + } + } + }; + + this.Given(x => x.GivenProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenProductServiceTwoIsRunning(downstreamServiceTwoUrl, 200)) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 50)) + .Then(x => x.ThenTheTwoServicesShouldHaveBeenCalledTimes(50)) + .And(x => x.ThenBothServicesCalledRealisticAmountOfTimes()) + .BDDfy(); + } + + private void ThenBothServicesCalledRealisticAmountOfTimes() + { + _counterOne.ShouldBe(25); + _counterTwo.ShouldBe(25); + } + + private void ThenTheTwoServicesShouldHaveBeenCalledTimes(int expected) + { + var total = _counterOne + _counterTwo; + total.ShouldBe(expected); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach(var serviceEntry in serviceEntries) + { + _serviceEntries.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if(context.Request.Path.Value == "/v1/health/service/product") + { + await context.Response.WriteJsonAsync(_serviceEntries); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + + private void GivenProductServiceOneIsRunning(string url, int statusCode) + { + _builderOne = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + try + { + var response = string.Empty; + lock (_syncLock) + { + _counterOne++; + response = _counterOne.ToString(); + } + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (System.Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + }) + .Build(); + + _builderOne.Start(); + } + + private void GivenProductServiceTwoIsRunning(string url, int statusCode) + { + _builderTwo = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + try + { + var response = string.Empty; + lock (_syncLock) + { + _counterTwo++; + response = _counterTwo.ToString(); + } + + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(response); + } + catch (System.Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + + }); + }) + .Build(); + + _builderTwo.Start(); + } + + public void Dispose() + { + _builderOne?.Dispose(); + _builderTwo?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 5f8f1eac..39b3465d 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -5,10 +5,13 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; using CacheManager.Core; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Ocelot.Configuration.File; @@ -29,6 +32,13 @@ namespace Ocelot.AcceptanceTests private BearerToken _token; public HttpClient OcelotClient => _ocelotClient; public string RequestIdKey = "OcRequestId"; + private readonly Random _random; + private IWebHostBuilder _webHostBuilder; + + public Steps() + { + _random = new Random(); + } public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) { @@ -61,12 +71,40 @@ namespace Ocelot.AcceptanceTests /// public void GivenOcelotIsRunning() { - _ocelotServer = new TestServer(new WebHostBuilder() + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder.ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + }); + + _ocelotServer = new TestServer(_webHostBuilder .UseStartup()); _ocelotClient = _ocelotServer.CreateClient(); } + internal void ThenTheResponseShouldBe(FileConfiguration expected) + { + var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); + + response.GlobalConfiguration.AdministrationPath.ShouldBe(expected.GlobalConfiguration.AdministrationPath); + response.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port); + response.GlobalConfiguration.ServiceDiscoveryProvider.Provider.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Provider); + + for(var i = 0; i < response.ReRoutes.Count; i++) + { + response.ReRoutes[i].DownstreamHost.ShouldBe(expected.ReRoutes[i].DownstreamHost); + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expected.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamPort.ShouldBe(expected.ReRoutes[i].DownstreamPort); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expected.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expected.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expected.ReRoutes[i].UpstreamHttpMethod); + } + } + /// /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. /// @@ -80,7 +118,14 @@ namespace Ocelot.AcceptanceTests var configuration = builder.Build(); - _ocelotServer = new TestServer(new WebHostBuilder() + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder.ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + }); + + _ocelotServer = new TestServer(_webHostBuilder .UseConfiguration(configuration) .ConfigureServices(s => { @@ -92,10 +137,9 @@ namespace Ocelot.AcceptanceTests }) .WithDictionaryHandle(); }; - + s.AddOcelotOutputCaching(settings); - s.AddOcelotFileConfiguration(configuration); - s.AddOcelot(); + s.AddOcelot(configuration); }) .ConfigureLogging(l => { @@ -104,7 +148,7 @@ namespace Ocelot.AcceptanceTests }) .Configure(a => { - a.UseOcelot(ocelotMiddlewareConfig); + a.UseOcelot(ocelotMiddlewareConfig).Wait(); })); _ocelotClient = _ocelotServer.CreateClient(); @@ -132,12 +176,32 @@ namespace Ocelot.AcceptanceTests using (var httpClient = new HttpClient()) { var response = httpClient.PostAsync(tokenUrl, content).Result; - response.EnsureSuccessStatusCode(); var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); _token = JsonConvert.DeserializeObject(responseContent); } } + public void GivenIHaveAnOcelotToken(string adminPath) + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("username", "admin"), + new KeyValuePair("password", "admin"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + var response = _ocelotClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + public void VerifyIdentiryServerStarted(string url) { using (var httpClient = new HttpClient()) @@ -147,12 +211,44 @@ namespace Ocelot.AcceptanceTests } } - public void WhenIGetUrlOnTheApiGateway(string url) { _response = _ocelotClient.GetAsync(url).Result; } + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) + { + var tasks = new Task[times]; + + for (int i = 0; i < times; i++) + { + var urlCopy = url; + tasks[i] = GetForServiceDiscoveryTest(urlCopy); + Thread.Sleep(_random.Next(40,60)); + } + + Task.WaitAll(tasks); + } + + private async Task GetForServiceDiscoveryTest(string url) + { + var response = await _ocelotClient.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + int count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } + + public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) + { + for (int i = 0; i < times; i++) + { + var clientId = "ocelotclient1"; + var request = new HttpRequestMessage(new HttpMethod("GET"), url); + request.Headers.Add("ClientId", clientId); + _response = _ocelotClient.SendAsync(request).Result; + } + } + public void WhenIGetUrlOnTheApiGateway(string url, string requestId) { _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); @@ -180,6 +276,13 @@ namespace Ocelot.AcceptanceTests _response.StatusCode.ShouldBe(expectedHttpStatusCode); } + + public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode) + { + var responseStatusCode = (int)_response.StatusCode; + responseStatusCode.ShouldBe(expectedHttpStatusCode); + } + public void Dispose() { _ocelotClient?.Dispose(); diff --git a/test/Ocelot.AcceptanceTests/Steps.cs.orig b/test/Ocelot.AcceptanceTests/Steps.cs.orig new file mode 100644 index 00000000..904c3969 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Steps.cs.orig @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using CacheManager.Core; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.ManualTest; +using Ocelot.Middleware; +using Shouldly; +using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; + +namespace Ocelot.AcceptanceTests +{ + public class Steps : IDisposable + { + private TestServer _ocelotServer; + private HttpClient _ocelotClient; + private HttpResponseMessage _response; + private HttpContent _postContent; + private BearerToken _token; + public HttpClient OcelotClient => _ocelotClient; + public string RequestIdKey = "OcRequestId"; + private readonly Random _random; + private IWebHostBuilder _webHostBuilder; + + public Steps() + { + _random = new Random(); + } + + public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = TestConfiguration.ConfigurationPath; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration, string configurationPath) + { + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + public void GivenOcelotIsRunning() + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder.ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + }); + + _ocelotServer = new TestServer(_webHostBuilder + .UseStartup()); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + internal void ThenTheResponseShouldBe(FileConfiguration expected) + { + var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); + + response.GlobalConfiguration.AdministrationPath.ShouldBe(expected.GlobalConfiguration.AdministrationPath); + response.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port); + response.GlobalConfiguration.ServiceDiscoveryProvider.Provider.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Provider); + + for(var i = 0; i < response.ReRoutes.Count; i++) + { + response.ReRoutes[i].DownstreamHost.ShouldBe(expected.ReRoutes[i].DownstreamHost); + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expected.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamPort.ShouldBe(expected.ReRoutes[i].DownstreamPort); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expected.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expected.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expected.ReRoutes[i].UpstreamHttpMethod); + } + } + + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + public void GivenOcelotIsRunning(OcelotMiddlewareConfiguration ocelotMiddlewareConfig) + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile("configuration.json") + .AddEnvironmentVariables(); + + var configuration = builder.Build(); + + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder.ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + }); + + _ocelotServer = new TestServer(_webHostBuilder + .UseConfiguration(configuration) + .ConfigureServices(s => + { + Action settings = (x) => + { + x.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; +<<<<<<< HEAD + +======= +>>>>>>> develop + s.AddOcelotOutputCaching(settings); + s.AddOcelot(configuration); + }) + .ConfigureLogging(l => + { + l.AddConsole(configuration.GetSection("Logging")); + l.AddDebug(); + }) + .Configure(a => + { + a.UseOcelot(ocelotMiddlewareConfig).Wait(); + })); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenIHaveAddedATokenToMyRequest() + { + _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + public void GivenIHaveAToken(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "client"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "api"), + new KeyValuePair("username", "test"), + new KeyValuePair("password", "test"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + using (var httpClient = new HttpClient()) + { + var response = httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + } + + public void GivenIHaveAnOcelotToken(string adminPath) + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("username", "admin"), + new KeyValuePair("password", "admin"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + var response = _ocelotClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + + public void VerifyIdentiryServerStarted(string url) + { + using (var httpClient = new HttpClient()) + { + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; + response.EnsureSuccessStatusCode(); + } + } + + public void WhenIGetUrlOnTheApiGateway(string url) + { + _response = _ocelotClient.GetAsync(url).Result; + } + + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) + { + var tasks = new Task[times]; + + for (int i = 0; i < times; i++) + { + var urlCopy = url; + tasks[i] = GetForServiceDiscoveryTest(urlCopy); + Thread.Sleep(_random.Next(40,60)); + } + + Task.WaitAll(tasks); + } + + private async Task GetForServiceDiscoveryTest(string url) + { + var response = await _ocelotClient.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + int count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } + + public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) + { + for (int i = 0; i < times; i++) + { + var clientId = "ocelotclient1"; + var request = new HttpRequestMessage(new HttpMethod("GET"), url); + request.Headers.Add("ClientId", clientId); + _response = _ocelotClient.SendAsync(request).Result; + } + } + + public void WhenIGetUrlOnTheApiGateway(string url, string requestId) + { + _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); + + _response = _ocelotClient.GetAsync(url).Result; + } + + public void WhenIPostUrlOnTheApiGateway(string url) + { + _response = _ocelotClient.PostAsync(url, _postContent).Result; + } + + public void GivenThePostHasContent(string postcontent) + { + _postContent = new StringContent(postcontent); + } + + public void ThenTheResponseBodyShouldBe(string expectedBody) + { + _response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody); + } + + public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + { + _response.StatusCode.ShouldBe(expectedHttpStatusCode); + } + + + public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode) + { + var responseStatusCode = (int)_response.StatusCode; + responseStatusCode.ShouldBe(expectedHttpStatusCode); + } + + public void Dispose() + { + _ocelotClient?.Dispose(); + _ocelotServer?.Dispose(); + } + + public void ThenTheRequestIdIsReturned() + { + _response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); + } + + public void ThenTheRequestIdIsReturned(string expected) + { + _response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/TestConfiguration.cs b/test/Ocelot.AcceptanceTests/TestConfiguration.cs index 69ccc690..3265955d 100644 --- a/test/Ocelot.AcceptanceTests/TestConfiguration.cs +++ b/test/Ocelot.AcceptanceTests/TestConfiguration.cs @@ -1,8 +1,10 @@ -namespace Ocelot.AcceptanceTests +using System; +using System.IO; + +namespace Ocelot.AcceptanceTests { public static class TestConfiguration { - public static double Version => 1.4; - public static string ConfigurationPath => $"./bin/Debug/netcoreapp{Version}/configuration.json"; + public static string ConfigurationPath => Path.Combine(AppContext.BaseDirectory, "configuration.json"); } } diff --git a/test/Ocelot.AcceptanceTests/TestConfiguration.cs.orig b/test/Ocelot.AcceptanceTests/TestConfiguration.cs.orig new file mode 100644 index 00000000..59b7ccd0 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/TestConfiguration.cs.orig @@ -0,0 +1,17 @@ +using System; +<<<<<<< HEAD +======= +using System.IO; +>>>>>>> develop + +namespace Ocelot.AcceptanceTests +{ + public static class TestConfiguration + { +<<<<<<< HEAD + public static string ConfigurationPath => $"{AppContext.BaseDirectory}/configuration.json"; +======= + public static string ConfigurationPath => Path.Combine(AppContext.BaseDirectory, "configuration.json"); +>>>>>>> develop + } +} diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json old mode 100644 new mode 100755 index 984f851c..b1e58121 --- 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},"ReRouteIsCaseSensitive":false}],"GlobalConfiguration":{"RequestIdKey":null}} \ No newline at end of file +{"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamPathTemplate":"/","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,"ServiceName":null,"DownstreamScheme":"http","DownstreamHost":"localhost","DownstreamPort":41879,"QoSOptions":{"ExceptionsAllowedBeforeBreaking":0,"DurationOfBreak":0,"TimeoutValue":0},"LoadBalancer":null,"RateLimitOptions":{"ClientWhitelist":[],"EnableRateLimiting":false,"Period":null,"PeriodTimespan":0.0,"Limit":0}}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"AdministrationPath":null,"RateLimitOptions":{"ClientIdHeader":"ClientId","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","DisableRateLimitHeaders":false,"HttpStatusCode":429}}} \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/project.json b/test/Ocelot.AcceptanceTests/project.json index dec6fd7b..7739992a 100644 --- a/test/Ocelot.AcceptanceTests/project.json +++ b/test/Ocelot.AcceptanceTests/project.json @@ -1,44 +1,50 @@ { - "version": "1.0.0-*", + "version": "0.0.0-dev", "buildOptions": { "copyToOutput": { - "include": [ - "configuration.json" - ] + "include": [ + "configuration.json" + ] } }, "testRunner": "xunit", - "dependencies": { - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0", - "Microsoft.Extensions.Configuration.Json": "1.0.0", - "Microsoft.Extensions.Logging": "1.0.0", - "Microsoft.Extensions.Logging.Console": "1.0.0", - "Microsoft.Extensions.Logging.Debug": "1.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", - "Microsoft.AspNetCore.Http": "1.0.0", - "Ocelot": "1.0.0-*", - "xunit": "2.2.0-beta2-build3300", - "dotnet-test-xunit": "2.2.0-preview2-build1029", - "Ocelot.ManualTest": "1.0.0-*", - "Microsoft.AspNetCore.TestHost": "1.0.0", - "IdentityServer4": "1.0.0-rc2", - "Microsoft.AspNetCore.Mvc": "1.0.1", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", - "Microsoft.NETCore.App": { - "version": "1.0.1", - "type": "platform" - }, - "Shouldly": "2.8.2", - "TestStack.BDDfy": "4.3.2" - }, - + "dependencies": { + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "Microsoft.DotNet.InternalAbstractions": "1.0.0", + "Ocelot": "0.0.0-dev", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Ocelot.ManualTest": "0.0.0-dev", + "Microsoft.AspNetCore.TestHost": "1.1.0", + "IdentityServer4": "1.0.1", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel.Https": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "Shouldly": "2.8.2", + "TestStack.BDDfy": "4.3.2", + "Consul": "0.7.2.1", + "Microsoft.Extensions.Caching.Memory": "1.1.0", + "xunit": "2.2.0-rc1-build3507" + }, + "runtimes": { + "osx.10.11-x64":{}, + "osx.10.12-x64":{}, + "win7-x64": {}, + "win10-x64": {} + }, "frameworks": { - "netcoreapp1.4": { + "netcoreapp1.1": { "imports": [ ] } diff --git a/test/Ocelot.AcceptanceTests/project.json.orig b/test/Ocelot.AcceptanceTests/project.json.orig new file mode 100644 index 00000000..5a30bc86 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/project.json.orig @@ -0,0 +1,59 @@ +{ + "version": "0.0.0-dev", + + "buildOptions": { + "copyToOutput": { + "include": [ + "configuration.json" + ] + } + }, + + "testRunner": "xunit", + + "dependencies": { + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "Microsoft.DotNet.InternalAbstractions": "1.0.0", + "Ocelot": "0.0.0-dev", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Ocelot.ManualTest": "0.0.0-dev", + "Microsoft.AspNetCore.TestHost": "1.1.0", + "IdentityServer4": "1.0.1", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel.Https": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "Shouldly": "2.8.2", + "TestStack.BDDfy": "4.3.2", + "Consul": "0.7.2.1", + "Microsoft.Extensions.Caching.Memory": "1.1.0", + "xunit": "2.2.0-rc1-build3507" + }, + "runtimes": { +<<<<<<< HEAD + "win10-x64": {}, + "osx.10.11-x64": {}, + "osx.10.12-x64": {}, + "win7-x64": {} +======= + "osx.10.11-x64":{}, + "osx.10.12-x64":{}, + "win7-x64": {}, + "win10-x64": {} +>>>>>>> develop + }, + "frameworks": { + "netcoreapp1.1": { + "imports": [ + ] + } + } +} diff --git a/test/Ocelot.Benchmarks/project.json b/test/Ocelot.Benchmarks/project.json index 791fea3d..615b9d0e 100644 --- a/test/Ocelot.Benchmarks/project.json +++ b/test/Ocelot.Benchmarks/project.json @@ -1,22 +1,23 @@ -{ - "version": "1.0.0-*", - "buildOptions": { - "emitEntryPoint": true - }, +{ + "version": "0.0.0-dev", + "buildOptions": { + "emitEntryPoint": true + }, - "dependencies": { - "Microsoft.NETCore.App": { - "type": "platform", - "version": "1.0.1" - }, - "Ocelot": "1.0.0-*", - "BenchmarkDotNet": "0.9.9" - }, - - "frameworks": { - "netcoreapp1.4": { - "imports": [ - ] - } + "dependencies": { + "Ocelot": "0.0.0-dev", + "BenchmarkDotNet": "0.10.2" + }, + "runtimes": { + "osx.10.11-x64":{}, + "osx.10.12-x64":{}, + "win7-x64": {}, + "win10-x64": {} + }, + "frameworks": { + "netcoreapp1.1": { + "imports": [ + ] } + } } diff --git a/test/Ocelot.Benchmarks/project.json.orig b/test/Ocelot.Benchmarks/project.json.orig new file mode 100644 index 00000000..ccff8b64 --- /dev/null +++ b/test/Ocelot.Benchmarks/project.json.orig @@ -0,0 +1,28 @@ +{ + "version": "0.0.0-dev", + "buildOptions": { + "emitEntryPoint": true + }, + + "dependencies": { + "Ocelot": "0.0.0-dev", + "BenchmarkDotNet": "0.10.2" + }, + "runtimes": { + "osx.10.11-x64":{}, +<<<<<<< HEAD + "osx.10.12-x64": {}, + "win7-x64": {} +======= + "osx.10.12-x64":{}, + "win7-x64": {}, + "win10-x64": {} +>>>>>>> develop + }, + "frameworks": { + "netcoreapp1.1": { + "imports": [ + ] + } + } +} diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs new file mode 100644 index 00000000..210c7ac5 --- /dev/null +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.ManualTest; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.IntegrationTests +{ + public class AdministrationTests : IDisposable + { + private readonly HttpClient _httpClient; + private HttpResponseMessage _response; + private IWebHost _builder; + private IWebHostBuilder _webHostBuilder; + private readonly string _ocelotBaseUrl; + private BearerToken _token; + + public AdministrationTests() + { + _httpClient = new HttpClient(); + _ocelotBaseUrl = "http://localhost:5000"; + _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); + } + + [Fact] + public void should_return_response_401_with_call_re_routes_controller() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + AdministrationPath = "/administration" + } + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_call_re_routes_controller() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + AdministrationPath = "/administration" + } + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_file_configuration() + { + var configuration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + AdministrationPath = "/administration", + RequestIdKey = "RequestId", + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "127.0.0.1", + Provider = "test" + } + + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHost = "localhost", + DownstreamPort = 80, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = "get", + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHost = "localhost", + DownstreamPort = 80, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = "get", + UpstreamPathTemplate = "/test" + } + } + }; + + this.Given(x => GivenThereIsAConfiguration(configuration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(configuration)) + .BDDfy(); + } + + [Fact] + public void should_get_file_configuration_edit_and_post_updated_version() + { + var initialConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + AdministrationPath = "/administration" + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHost = "localhost", + DownstreamPort = 80, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = "get", + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHost = "localhost", + DownstreamPort = 80, + DownstreamScheme = "https", + DownstreamPathTemplate = "/", + UpstreamHttpMethod = "get", + UpstreamPathTemplate = "/test" + } + } + }; + + var updatedConfiguration = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + AdministrationPath = "/administration" + }, + ReRoutes = new List() + { + new FileReRoute() + { + DownstreamHost = "127.0.0.1", + DownstreamPort = 80, + DownstreamScheme = "http", + DownstreamPathTemplate = "/geoffrey", + UpstreamHttpMethod = "get", + UpstreamPathTemplate = "/" + }, + new FileReRoute() + { + DownstreamHost = "123.123.123", + DownstreamPort = 443, + DownstreamScheme = "https", + DownstreamPathTemplate = "/blooper/{productId}", + UpstreamHttpMethod = "post", + UpstreamPathTemplate = "/test" + } + } + }; + + this.Given(x => GivenThereIsAConfiguration(initialConfiguration)) + .And(x => GivenOcelotIsRunning()) + .And(x => GivenIHaveAnOcelotToken("/administration")) + .And(x => GivenIHaveAddedATokenToMyRequest()) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .When(x => WhenIPostOnTheApiGateway("/administration/configuration", updatedConfiguration)) + .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .When(x => WhenIGetUrlOnTheApiGateway("/administration/configuration")) + .And(x => ThenTheResponseShouldBe(updatedConfiguration)) + .BDDfy(); + } + + private void WhenIPostOnTheApiGateway(string url, FileConfiguration updatedConfiguration) + { + var json = JsonConvert.SerializeObject(updatedConfiguration); + var content = new StringContent(json); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + _response = _httpClient.PostAsync(url, content).Result; + } + + private void ThenTheResponseShouldBe(FileConfiguration expected) + { + var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); + + response.GlobalConfiguration.AdministrationPath.ShouldBe(expected.GlobalConfiguration.AdministrationPath); + response.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey); + response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host); + response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port); + response.GlobalConfiguration.ServiceDiscoveryProvider.Provider.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Provider); + + for (var i = 0; i < response.ReRoutes.Count; i++) + { + response.ReRoutes[i].DownstreamHost.ShouldBe(expected.ReRoutes[i].DownstreamHost); + response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expected.ReRoutes[i].DownstreamPathTemplate); + response.ReRoutes[i].DownstreamPort.ShouldBe(expected.ReRoutes[i].DownstreamPort); + response.ReRoutes[i].DownstreamScheme.ShouldBe(expected.ReRoutes[i].DownstreamScheme); + response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expected.ReRoutes[i].UpstreamPathTemplate); + response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expected.ReRoutes[i].UpstreamHttpMethod); + } + } + + private void GivenIHaveAddedATokenToMyRequest() + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + private void GivenIHaveAnOcelotToken(string adminPath) + { + var tokenUrl = $"{adminPath}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "admin"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "admin"), + new KeyValuePair("username", "admin"), + new KeyValuePair("password", "secret"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + var response = _httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + + private void GivenOcelotIsRunning() + { + _webHostBuilder = new WebHostBuilder() + .UseUrls(_ocelotBaseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureServices(x => { + x.AddSingleton(_webHostBuilder); + }) + .UseStartup(); + + _builder = _webHostBuilder.Build(); + + _builder.Start(); + } + + private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = $"{Directory.GetCurrentDirectory()}/configuration.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + var text = File.ReadAllText(configurationPath); + + configurationPath = $"{AppContext.BaseDirectory}/configuration.json"; + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + + text = File.ReadAllText(configurationPath); + } + + private void WhenIGetUrlOnTheApiGateway(string url) + { + _response = _httpClient.GetAsync(url).Result; + } + + private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + { + _response.StatusCode.ShouldBe(expectedHttpStatusCode); + } + + public void Dispose() + { + _builder?.Dispose(); + _httpClient?.Dispose(); + } + } +} diff --git a/test/Ocelot.IntegrationTests/BearerToken.cs b/test/Ocelot.IntegrationTests/BearerToken.cs new file mode 100644 index 00000000..efd35c33 --- /dev/null +++ b/test/Ocelot.IntegrationTests/BearerToken.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Ocelot.IntegrationTests +{ + class BearerToken + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + + [JsonProperty("token_type")] + public string TokenType { get; set; } + } +} diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.xproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.xproj new file mode 100644 index 00000000..ece2b3b5 --- /dev/null +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + d4575572-99ca-4530-8737-c296eda326f8 + Ocelot.IntegrationTests + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Ocelot.IntegrationTests/Properties/AssemblyInfo.cs b/test/Ocelot.IntegrationTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..5535cd70 --- /dev/null +++ b/test/Ocelot.IntegrationTests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Ocelot.IntegrationTests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d4575572-99ca-4530-8737-c296eda326f8")] diff --git a/test/Ocelot.IntegrationTests/appsettings.json b/test/Ocelot.IntegrationTests/appsettings.json new file mode 100644 index 00000000..d73b7dcb --- /dev/null +++ b/test/Ocelot.IntegrationTests/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": true, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/test/Ocelot.IntegrationTests/configuration.json b/test/Ocelot.IntegrationTests/configuration.json new file mode 100755 index 00000000..2ce0beff --- /dev/null +++ b/test/Ocelot.IntegrationTests/configuration.json @@ -0,0 +1 @@ +{"ReRoutes":[],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"AdministrationPath":"/administration","RateLimitOptions":{"ClientIdHeader":"ClientId","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","DisableRateLimitHeaders":false,"HttpStatusCode":429}}} \ No newline at end of file diff --git a/test/Ocelot.IntegrationTests/project.json b/test/Ocelot.IntegrationTests/project.json new file mode 100644 index 00000000..578f9a7f --- /dev/null +++ b/test/Ocelot.IntegrationTests/project.json @@ -0,0 +1,51 @@ +{ + "version": "0.0.0-dev", + + "buildOptions": { + "copyToOutput": { + "include": [ + "configuration.json", + "appsettings.json" + ] + } + }, + + "testRunner": "xunit", + + "dependencies": { + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "Microsoft.DotNet.InternalAbstractions": "1.0.0", + "Ocelot": "0.0.0-dev", + "xunit": "2.2.0-beta2-build3300", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Ocelot.ManualTest": "0.0.0-dev", + "Microsoft.AspNetCore.TestHost": "1.1.0", + "IdentityServer4": "1.0.1", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "Shouldly": "2.8.2", + "TestStack.BDDfy": "4.3.2", + "Consul": "0.7.2.1" + }, + "runtimes": { + "win10-x64": {}, + "osx.10.11-x64": {}, + "osx.10.12-x64": {}, + "win7-x64": {} + }, + "frameworks": { + "netcoreapp1.1": { + "imports": [ + ] + } + } +} diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 7b6b8535..a545819a 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -1,5 +1,6 @@ using System.IO; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; namespace Ocelot.ManualTest { @@ -7,12 +8,17 @@ namespace Ocelot.ManualTest { public static void Main(string[] args) { - var host = new WebHostBuilder() - .UseKestrel() + IWebHostBuilder builder = new WebHostBuilder(); + + builder.ConfigureServices(s => { + s.AddSingleton(builder); + }); + + builder.UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseStartup() - .Build(); + .UseStartup(); + + var host = builder.Build(); host.Run(); } diff --git a/test/Ocelot.ManualTest/Startup.cs b/test/Ocelot.ManualTest/Startup.cs index 70448fcf..26adc927 100644 --- a/test/Ocelot.ManualTest/Startup.cs +++ b/test/Ocelot.ManualTest/Startup.cs @@ -37,17 +37,15 @@ namespace Ocelot.ManualTest }) .WithDictionaryHandle(); }; - services.AddOcelotOutputCaching(settings); - services.AddOcelotFileConfiguration(Configuration); - services.AddOcelot(); + services.AddOcelot(Configuration); } - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); - app.UseOcelot(); + await app.UseOcelot(); } } } diff --git a/test/Ocelot.ManualTest/configuration.json b/test/Ocelot.ManualTest/configuration.json index 15276d6e..980614bd 100644 --- a/test/Ocelot.ManualTest/configuration.json +++ b/test/Ocelot.ManualTest/configuration.json @@ -1,151 +1,306 @@ { - "ReRoutes": [ - { - "DownstreamTemplate": "http://localhost:52876/", - "UpstreamTemplate": "/identityserverexample", - "UpstreamHttpMethod": "Get", - "AuthenticationOptions": { - "Provider": "IdentityServer", - "ProviderRootUrl": "http://localhost:52888", - "ScopeName": "api", - "AdditionalScopes": [ - "openid", - "offline_access" - ], - "ScopeSecret": "secret" - }, - "AddHeadersToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - "AddClaimsToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - "AddQueriesToRequest": { - "CustomerId": "Claims[CustomerId] > value", - "LocationId": "Claims[LocationId] > value", - "UserType": "Claims[sub] > value[0] > |", - "UserId": "Claims[sub] > value[1] > |" - }, - "RouteClaimsRequirement": { - "UserType": "registered" - }, - "RequestIdKey": "OcRequestId" - }, - { - "DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts", - "UpstreamTemplate": "/posts", - "UpstreamHttpMethod": "Get", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts/{postId}", - "UpstreamTemplate": "/posts/{postId}", - "UpstreamHttpMethod": "Get" - }, - { - "DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts/{postId}/comments", - "UpstreamTemplate": "/posts/{postId}/comments", - "UpstreamHttpMethod": "Get" - }, - { - "DownstreamTemplate": "http://jsonplaceholder.typicode.com/comments", - "UpstreamTemplate": "/comments", - "UpstreamHttpMethod": "Get" - }, - { - "DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts", - "UpstreamTemplate": "/posts", - "UpstreamHttpMethod": "Post" - }, - { - "DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts/{postId}", - "UpstreamTemplate": "/posts/{postId}", - "UpstreamHttpMethod": "Put" - }, - { - "DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts/{postId}", - "UpstreamTemplate": "/posts/{postId}", - "UpstreamHttpMethod": "Patch" - }, - { - "DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts/{postId}", - "UpstreamTemplate": "/posts/{postId}", - "UpstreamHttpMethod": "Delete" - }, - { - "DownstreamTemplate": "http://products20161126090340.azurewebsites.net/api/products", - "UpstreamTemplate": "/products", - "UpstreamHttpMethod": "Get", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://products20161126090340.azurewebsites.net/api/products/{productId}", - "UpstreamTemplate": "/products/{productId}", - "UpstreamHttpMethod": "Get", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://products20161126090340.azurewebsites.net/api/products", - "UpstreamTemplate": "/products", - "UpstreamHttpMethod": "Post", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://products20161126090340.azurewebsites.net/api/products/{productId}", - "UpstreamTemplate": "/products/{productId}", - "UpstreamHttpMethod": "Put", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://products20161126090340.azurewebsites.net/api/products/{productId}", - "UpstreamTemplate": "/products/{productId}", - "UpstreamHttpMethod": "Delete", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://customers20161126090811.azurewebsites.net/api/customers", - "UpstreamTemplate": "/customers", - "UpstreamHttpMethod": "Get", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://customers20161126090811.azurewebsites.net/api/customers/{customerId}", - "UpstreamTemplate": "/customers/{customerId}", - "UpstreamHttpMethod": "Get", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://customers20161126090811.azurewebsites.net/api/customers", - "UpstreamTemplate": "/customers", - "UpstreamHttpMethod": "Post", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://customers20161126090811.azurewebsites.net/api/customers/{customerId}", - "UpstreamTemplate": "/customers/{customerId}", - "UpstreamHttpMethod": "Put", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://customers20161126090811.azurewebsites.net/api/customers/{customerId}", - "UpstreamTemplate": "/customers/{customerId}", - "UpstreamHttpMethod": "Delete", - "FileCacheOptions": { "TtlSeconds": 15 } - }, - { - "DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts", - "UpstreamTemplate": "/posts/", - "UpstreamHttpMethod": "Get", - "FileCacheOptions": { "TtlSeconds": 15 } - } - ], - "GlobalConfiguration": { - "RequestIdKey": "OcRequestId" + "ReRoutes": [ + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHost": "localhost", + "DownstreamPort": 52876, + "UpstreamPathTemplate": "/identityserverexample", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "AuthenticationOptions": { + "Provider": "IdentityServer", + "ProviderRootUrl": "http://localhost:52888", + "ScopeName": "api", + "AdditionalScopes": [ + "openid", + "offline_access" + ], + "ScopeSecret": "secret" + }, + "AddHeadersToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "AddClaimsToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "AddQueriesToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "RouteClaimsRequirement": { + "UserType": "registered" + }, + "RequestIdKey": "OcRequestId" + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "https", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 443, + "UpstreamPathTemplate": "/posts", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}/comments", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/{postId}/comments", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/comments", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/comments", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts", + "UpstreamHttpMethod": "Post", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Put", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Patch", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Delete", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/api/products", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/products", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/products/{productId}", + "UpstreamHttpMethod": "Get", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products", + "DownstreamScheme": "http", + "DownstreamHost": "products20161126090340.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/products", + "UpstreamHttpMethod": "Post", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHost": "products20161126090340.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/products/{productId}", + "UpstreamHttpMethod": "Put", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHost": "products20161126090340.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/products/{productId}", + "UpstreamHttpMethod": "Delete", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/customers", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers/{customerId}", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/customers/{customerId}", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/customers", + "UpstreamHttpMethod": "Post", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers/{customerId}", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/customers/{customerId}", + "UpstreamHttpMethod": "Put", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers/{customerId}", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/customers/{customerId}", + "UpstreamHttpMethod": "Delete", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } } + ], + + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "AdministrationPath": "/admin" + } } \ No newline at end of file diff --git a/test/Ocelot.ManualTest/configuration.json.orig b/test/Ocelot.ManualTest/configuration.json.orig new file mode 100644 index 00000000..57db2995 --- /dev/null +++ b/test/Ocelot.ManualTest/configuration.json.orig @@ -0,0 +1,310 @@ +{ + "ReRoutes": [ + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHost": "localhost", + "DownstreamPort": 52876, + "UpstreamPathTemplate": "/identityserverexample", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "AuthenticationOptions": { + "Provider": "IdentityServer", + "ProviderRootUrl": "http://localhost:52888", + "ScopeName": "api", + "AdditionalScopes": [ + "openid", + "offline_access" + ], + "ScopeSecret": "secret" + }, + "AddHeadersToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "AddClaimsToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "AddQueriesToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "RouteClaimsRequirement": { + "UserType": "registered" + }, + "RequestIdKey": "OcRequestId" + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "https", + "DownstreamHost": "jsonplaceholder.typicode.com", +<<<<<<< HEAD + "DownstreamPort": 80, +======= + "DownstreamPort": 443, +>>>>>>> develop + "UpstreamPathTemplate": "/posts", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}/comments", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/{postId}/comments", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/comments", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/comments", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts", + "UpstreamHttpMethod": "Post", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Put", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Patch", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Delete", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/api/products", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/products", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/products/{productId}", + "UpstreamHttpMethod": "Get", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products", + "DownstreamScheme": "http", + "DownstreamHost": "products20161126090340.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/products", + "UpstreamHttpMethod": "Post", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHost": "products20161126090340.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/products/{productId}", + "UpstreamHttpMethod": "Put", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHost": "products20161126090340.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/products/{productId}", + "UpstreamHttpMethod": "Delete", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/customers", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers/{customerId}", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/customers/{customerId}", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/customers", + "UpstreamHttpMethod": "Post", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers/{customerId}", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/customers/{customerId}", + "UpstreamHttpMethod": "Put", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers/{customerId}", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/customers/{customerId}", + "UpstreamHttpMethod": "Delete", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamPathTemplate": "/posts/", + "UpstreamHttpMethod": "Get", + "QoSOptions": { + "ExceptionsAllowedBeforeBreaking": 3, + "DurationOfBreak": 10, + "TimeoutValue": 5000 + }, + "FileCacheOptions": { "TtlSeconds": 15 } + } + ], + + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "AdministrationPath": "/admin" + } +} \ No newline at end of file diff --git a/test/Ocelot.ManualTest/project.json b/test/Ocelot.ManualTest/project.json index 65220938..5083532b 100644 --- a/test/Ocelot.ManualTest/project.json +++ b/test/Ocelot.ManualTest/project.json @@ -1,64 +1,67 @@ { - "version": "1.0.0-*", + "version": "0.0.0-dev", - "dependencies": { - "Microsoft.AspNetCore.Http": "1.0.0", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0", - "Microsoft.Extensions.Configuration.Json": "1.0.0", - "Microsoft.Extensions.Logging": "1.0.0", - "Microsoft.Extensions.Logging.Console": "1.0.0", - "Microsoft.Extensions.Logging.Debug": "1.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", - "Ocelot": "1.0.0-*", - "Microsoft.AspNetCore.Mvc": "1.0.1", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", - "Microsoft.NETCore.App": { - "version": "1.0.1", - "type": "platform" - } - }, + "dependencies": { + "Microsoft.AspNetCore.Http": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Ocelot": "0.0.0-dev", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "Consul": "0.7.2.1", + "Polly": "5.0.3", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0" + }, - "tools": { - "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" - }, - - "frameworks": { - "netcoreapp1.4": { - "imports": [ - ] - } - }, - - "buildOptions": { - "emitEntryPoint": true, - "preserveCompilationContext": true, - "copyToOutput": { - "include": [ - "configuration.json" - ] - } - }, - - "runtimeOptions": { - "configProperties": { - "System.GC.Server": true - } - }, - - "publishOptions": { - "include": [ - "wwwroot", - "Views", - "Areas/**/Views", - "appsettings.json", - "web.config", - "configuration.json" - ] - }, - - "scripts": { - "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] - } + "tools": { + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" + }, + "runtimes": { + "osx.10.11-x64":{}, + "osx.10.12-x64":{}, + "win7-x64": {}, + "win10-x64": {} + }, + "frameworks": { + "netcoreapp1.1": { + "imports": [ + ] } + }, + + "buildOptions": { + "emitEntryPoint": true, + "preserveCompilationContext": true, + "copyToOutput": { + "include": [ + "configuration.json" + ] + } + }, + + "runtimeOptions": { + "configProperties": { + "System.GC.Server": true + } + }, + + "publishOptions": { + "include": [ + "wwwroot", + "Views", + "Areas/**/Views", + "appsettings.json", + "web.config", + "configuration.json" + ] + }, + + "scripts": { + "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] + } +} diff --git a/test/Ocelot.ManualTest/project.json.orig b/test/Ocelot.ManualTest/project.json.orig new file mode 100644 index 00000000..6824e88f --- /dev/null +++ b/test/Ocelot.ManualTest/project.json.orig @@ -0,0 +1,72 @@ +{ + "version": "0.0.0-dev", + + "dependencies": { + "Microsoft.AspNetCore.Http": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Ocelot": "0.0.0-dev", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "Consul": "0.7.2.1", + "Polly": "5.0.3", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0" + }, + + "tools": { + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" + }, + "runtimes": { + "osx.10.11-x64":{}, +<<<<<<< HEAD + "osx.10.12-x64": {}, + "win7-x64": {} +======= + "osx.10.12-x64":{}, + "win7-x64": {}, + "win10-x64": {} +>>>>>>> develop + }, + "frameworks": { + "netcoreapp1.1": { + "imports": [ + ] + } + }, + + "buildOptions": { + "emitEntryPoint": true, + "preserveCompilationContext": true, + "copyToOutput": { + "include": [ + "configuration.json" + ] + } + }, + + "runtimeOptions": { + "configProperties": { + "System.GC.Server": true + } + }, + + "publishOptions": { + "include": [ + "wwwroot", + "Views", + "Areas/**/Views", + "appsettings.json", + "web.config", + "configuration.json" + ] + }, + + "scripts": { + "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] + } +} diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationHandlerFactoryTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationHandlerFactoryTests.cs index e76a2b28..8bf53607 100644 --- a/test/Ocelot.UnitTests/Authentication/AuthenticationHandlerFactoryTests.cs +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationHandlerFactoryTests.cs @@ -6,6 +6,7 @@ using Moq; using Ocelot.Authentication.Handler; using Ocelot.Authentication.Handler.Creator; using Ocelot.Authentication.Handler.Factory; +using Ocelot.Configuration.Builder; using Ocelot.Errors; using Ocelot.Responses; using Shouldly; @@ -33,7 +34,11 @@ namespace Ocelot.UnitTests.Authentication [Fact] public void should_return_identity_server_access_token_handler() { - this.Given(x => x.GivenTheAuthenticationOptionsAre(new AuthenticationOptions("IdentityServer", "","",false, new List(), ""))) + var authenticationOptions = new AuthenticationOptionsBuilder() + .WithProvider("IdentityServer") + .Build(); + + this.Given(x => x.GivenTheAuthenticationOptionsAre(authenticationOptions)) .And(x => x.GivenTheCreatorReturns()) .When(x => x.WhenIGetFromTheFactory()) .Then(x => x.ThenTheHandlerIsReturned("IdentityServer")) @@ -43,7 +48,10 @@ namespace Ocelot.UnitTests.Authentication [Fact] public void should_return_error_if_cannot_create_handler() { - this.Given(x => x.GivenTheAuthenticationOptionsAre(new AuthenticationOptions("IdentityServer", "", "", false, new List(), ""))) + var authenticationOptions = new AuthenticationOptionsBuilder() + .Build(); + + this.Given(x => x.GivenTheAuthenticationOptionsAre(authenticationOptions)) .And(x => x.GivenTheCreatorReturnsAnError()) .When(x => x.WhenIGetFromTheFactory()) .Then(x => x.ThenAnErrorResponseIsReturned()) @@ -58,7 +66,7 @@ namespace Ocelot.UnitTests.Authentication private void GivenTheCreatorReturnsAnError() { _creator - .Setup(x => x.CreateIdentityServerAuthenticationHandler(It.IsAny(), It.IsAny())) + .Setup(x => x.Create(It.IsAny(), It.IsAny())) .Returns(new ErrorResponse(new List { new UnableToCreateAuthenticationHandlerError($"Unable to create authentication handler for xxx") @@ -68,7 +76,7 @@ namespace Ocelot.UnitTests.Authentication private void GivenTheCreatorReturns() { _creator - .Setup(x => x.CreateIdentityServerAuthenticationHandler(It.IsAny(), It.IsAny())) + .Setup(x => x.Create(It.IsAny(), It.IsAny())) .Returns(new OkResponse(x => Task.CompletedTask)); } diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs index ff467aa7..f60eb83c 100644 --- a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs @@ -71,7 +71,9 @@ namespace Ocelot.UnitTests.Authentication [Fact] public void should_call_next_middleware_if_route_is_not_authenticated() { - this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), new ReRouteBuilder().Build()))) + this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), new ReRouteBuilder() + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheUserIsAuthenticated()) .BDDfy(); diff --git a/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs index 8d681e03..35435fc2 100644 --- a/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs @@ -63,7 +63,11 @@ namespace Ocelot.UnitTests.Authorization [Fact] public void should_call_authorisation_service() { - this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), new ReRouteBuilder().WithIsAuthorised(true).Build()))) + this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithIsAuthorised(true) + .WithUpstreamHttpMethod("Get") .WithUpstreamHttpMethod("Get") + .Build()))) .And(x => x.GivenTheAuthServiceReturns(new OkResponse(true))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheAuthServiceIsCalledCorrectly()) diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index 9190dbe9..66532a18 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -87,7 +87,12 @@ namespace Ocelot.UnitTests.Cache private void GivenTheDownstreamRouteIs() { - var reRoute = new ReRouteBuilder().WithIsCached(true).WithCacheOptions(new CacheOptions(100)).Build(); + var reRoute = new ReRouteBuilder() + .WithIsCached(true) + .WithCacheOptions(new CacheOptions(100)) + .WithUpstreamHttpMethod("Get") + .Build(); + var downstreamRoute = new DownstreamRoute(new List(), reRoute); _scopedRepo diff --git a/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs index e3cccdc0..90a16b17 100644 --- a/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs @@ -67,11 +67,12 @@ namespace Ocelot.UnitTests.Claims { var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() - .WithDownstreamTemplate("any old string") + .WithDownstreamPathTemplate("any old string") .WithClaimsToClaims(new List { new ClaimToThing("sub", "UserType", "|", 0) }) + .WithUpstreamHttpMethod("Get") .Build()); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs index 7f6f4005..382471c6 100644 --- a/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs @@ -10,8 +10,8 @@ namespace Ocelot.UnitTests.Configuration { public class ConfigurationValidationTests { - private FileConfiguration _fileConfiguration; private readonly IConfigurationValidator _configurationValidator; + private FileConfiguration _fileConfiguration; private Response _result; public ConfigurationValidationTests() @@ -20,16 +20,35 @@ namespace Ocelot.UnitTests.Configuration } [Fact] - public void configuration_is_valid_with_one_reroute() + public void configuration_is_invalid_if_scheme_in_downstream_template() { - this.Given(x => x.GivenAConfiguration(new FileConfiguration() + this.Given(x => x.GivenAConfiguration(new FileConfiguration { ReRoutes = new List { new FileReRoute { - DownstreamTemplate = "http://www.bbc.co.uk", - UpstreamTemplate = "http://asdf.com" + DownstreamPathTemplate = "http://www.bbc.co.uk/api/products/{productId}", + UpstreamPathTemplate = "http://asdf.com" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_one_reroute() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "http://asdf.com" } } })) @@ -41,14 +60,14 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void configuration_is_valid_with_valid_authentication_provider() { - this.Given(x => x.GivenAConfiguration(new FileConfiguration() + this.Given(x => x.GivenAConfiguration(new FileConfiguration { ReRoutes = new List { new FileReRoute { - DownstreamTemplate = "http://www.bbc.co.uk", - UpstreamTemplate = "http://asdf.com", + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "http://asdf.com", AuthenticationOptions = new FileAuthenticationOptions { Provider = "IdentityServer" @@ -64,14 +83,14 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void configuration_is_invalid_with_invalid_authentication_provider() { - this.Given(x => x.GivenAConfiguration(new FileConfiguration() + this.Given(x => x.GivenAConfiguration(new FileConfiguration { ReRoutes = new List { new FileReRoute { - DownstreamTemplate = "http://www.bbc.co.uk", - UpstreamTemplate = "http://asdf.com", + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "http://asdf.com", AuthenticationOptions = new FileAuthenticationOptions { Provider = "BootyBootyBottyRockinEverywhere" @@ -88,25 +107,25 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void configuration_is_not_valid_with_duplicate_reroutes() { - this.Given(x => x.GivenAConfiguration(new FileConfiguration() + this.Given(x => x.GivenAConfiguration(new FileConfiguration { ReRoutes = new List { new FileReRoute { - DownstreamTemplate = "http://www.bbc.co.uk", - UpstreamTemplate = "http://asdf.com" + DownstreamPathTemplate = "/api/products/", + UpstreamPathTemplate = "http://asdf.com" }, new FileReRoute { - DownstreamTemplate = "http://www.bbc.co.uk", - UpstreamTemplate = "http://asdf.com" + DownstreamPathTemplate = "http://www.bbc.co.uk", + UpstreamPathTemplate = "http://asdf.com" } } })) .When(x => x.WhenIValidateTheConfiguration()) .Then(x => x.ThenTheResultIsNotValid()) - .And(x => x.ThenTheErrorIs()) + .And(x => x.ThenTheErrorIs()) .BDDfy(); } @@ -135,4 +154,4 @@ namespace Ocelot.UnitTests.Configuration _result.Data.Errors[0].ShouldBeOfType(); } } -} +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 4a4a5345..fa1bbda3 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -8,6 +8,8 @@ using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Validator; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Requester.QoS; using Ocelot.Responses; using Shouldly; using TestStack.BDDfy; @@ -24,15 +26,223 @@ namespace Ocelot.UnitTests.Configuration private readonly Mock _configParser; private readonly Mock> _logger; private readonly FileOcelotConfigurationCreator _ocelotConfigurationCreator; + private readonly Mock _loadBalancerFactory; + private readonly Mock _loadBalancerHouse; + private readonly Mock _loadBalancer; + private readonly Mock _qosProviderFactory; + private readonly Mock _qosProviderHouse; + private readonly Mock _qosProvider; public FileConfigurationCreatorTests() { + _qosProviderFactory = new Mock(); + _qosProviderHouse = new Mock(); + _qosProvider = new Mock(); _logger = new Mock>(); _configParser = new Mock(); _validator = new Mock(); _fileConfig = new Mock>(); + _loadBalancerFactory = new Mock(); + _loadBalancerHouse = new Mock(); + _loadBalancer = new Mock(); _ocelotConfigurationCreator = new FileOcelotConfigurationCreator( - _fileConfig.Object, _validator.Object, _configParser.Object, _logger.Object); + _fileConfig.Object, _validator.Object, _configParser.Object, _logger.Object, + _loadBalancerFactory.Object, _loadBalancerHouse.Object, + _qosProviderFactory.Object, _qosProviderHouse.Object); + } + + [Fact] + public void should_create_load_balancer() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHost = "127.0.0.1", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => x.GivenTheLoadBalancerFactoryReturns()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.TheLoadBalancerFactoryIsCalledCorrectly()) + .And(x => x.ThenTheLoadBalancerHouseIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_create_qos_provider() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHost = "127.0.0.1", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1, + DurationOfBreak = 1, + ExceptionsAllowedBeforeBreaking = 1 + } + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => x.GivenTheQosProviderFactoryReturns()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.TheQosProviderFactoryIsCalledCorrectly()) + .And(x => x.ThenTheQosProviderHouseIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_use_downstream_host() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHost = "127.0.0.1", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/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") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_use_downstream_scheme() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamScheme = "https", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamScheme("https") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/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 + { + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = false, + ServiceName = "ProductService" + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Provider = "consul", + Host = "127.0.0.1" + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .WithServiceProviderConfiguraion(new ServiceProviderConfiguraionBuilder() + .WithUseServiceDiscovery(true) + .WithServiceDiscoveryProvider("consul") + .WithServiceDiscoveryProviderHost("127.0.0.1") + .WithServiceName("ProductService") + .Build()) + .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 + { + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = false, + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .WithServiceProviderConfiguraion(new ServiceProviderConfiguraionBuilder() + .WithUseServiceDiscovery(false) + .Build()) + .Build() + })) + .BDDfy(); } [Fact] @@ -44,8 +254,8 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - UpstreamTemplate = "/api/products/{productId}", - DownstreamTemplate = "/products/{productId}", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", ReRouteIsCaseSensitive = false } @@ -56,8 +266,8 @@ namespace Ocelot.UnitTests.Configuration .Then(x => x.ThenTheReRoutesAre(new List { new ReRouteBuilder() - .WithDownstreamTemplate("/products/{productId}") - .WithUpstreamTemplate("/api/products/{productId}") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") .Build() @@ -74,8 +284,8 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - UpstreamTemplate = "/api/products/{productId}", - DownstreamTemplate = "/products/{productId}", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get" } } @@ -85,8 +295,8 @@ namespace Ocelot.UnitTests.Configuration .Then(x => x.ThenTheReRoutesAre(new List { new ReRouteBuilder() - .WithDownstreamTemplate("/products/{productId}") - .WithUpstreamTemplate("/api/products/{productId}") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") .Build() @@ -103,8 +313,8 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - UpstreamTemplate = "/api/products/{productId}", - DownstreamTemplate = "/products/{productId}", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", ReRouteIsCaseSensitive = true } @@ -115,8 +325,8 @@ namespace Ocelot.UnitTests.Configuration .Then(x => x.ThenTheReRoutesAre(new List { new ReRouteBuilder() - .WithDownstreamTemplate("/products/{productId}") - .WithUpstreamTemplate("/api/products/{productId}") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("/api/products/.*/$") .Build() @@ -133,8 +343,8 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - UpstreamTemplate = "/api/products/{productId}", - DownstreamTemplate = "/products/{productId}", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", ReRouteIsCaseSensitive = true } @@ -149,8 +359,8 @@ namespace Ocelot.UnitTests.Configuration .Then(x => x.ThenTheReRoutesAre(new List { new ReRouteBuilder() - .WithDownstreamTemplate("/products/{productId}") - .WithUpstreamTemplate("/api/products/{productId}") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("/api/products/.*/$") .WithRequestIdKey("blahhhh") @@ -168,8 +378,8 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - UpstreamTemplate = "/api/products/{productId}", - DownstreamTemplate = "/products/{productId}", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", ReRouteIsCaseSensitive = true } @@ -180,8 +390,8 @@ namespace Ocelot.UnitTests.Configuration .Then(x => x.ThenTheReRoutesAre(new List { new ReRouteBuilder() - .WithDownstreamTemplate("/products/{productId}") - .WithUpstreamTemplate("/api/products/{productId}") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("/api/products/.*/$") .Build() @@ -192,18 +402,23 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void should_create_with_headers_to_extract() { + var authenticationOptions = new AuthenticationOptionsBuilder() + .WithProvider("IdentityServer") + .WithProviderRootUrl("http://localhost:51888") + .WithRequireHttps(false) + .WithScopeSecret("secret") + .WithScopeName("api") + .WithAdditionalScopes(new List()) + .Build(); + var expected = new List { new ReRouteBuilder() - .WithDownstreamTemplate("/products/{productId}") - .WithUpstreamTemplate("/api/products/{productId}") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("/api/products/.*/$") - .WithAuthenticationProvider("IdentityServer") - .WithAuthenticationProviderUrl("http://localhost:51888") - .WithRequireHttps(false) - .WithScopeSecret("secret") - .WithAuthenticationProviderScopeName("api") + .WithAuthenticationOptions(authenticationOptions) .WithClaimsToHeaders(new List { new ClaimToThing("CustomerId", "CustomerId", "", 0), @@ -217,8 +432,8 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - UpstreamTemplate = "/api/products/{productId}", - DownstreamTemplate = "/products/{productId}", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", ReRouteIsCaseSensitive = true, AuthenticationOptions = new FileAuthenticationOptions @@ -239,6 +454,7 @@ namespace Ocelot.UnitTests.Configuration })) .And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheConfigHeaderExtractorReturns(new ClaimToThing("CustomerId", "CustomerId", "", 0))) + .And(x => x.GivenTheLoadBalancerFactoryReturns()) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(expected)) .And(x => x.ThenTheAuthenticationOptionsAre(expected)) @@ -255,18 +471,23 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void should_create_with_authentication_properties() { + var authenticationOptions = new AuthenticationOptionsBuilder() + .WithProvider("IdentityServer") + .WithProviderRootUrl("http://localhost:51888") + .WithRequireHttps(false) + .WithScopeSecret("secret") + .WithScopeName("api") + .WithAdditionalScopes(new List()) + .Build(); + var expected = new List { new ReRouteBuilder() - .WithDownstreamTemplate("/products/{productId}") - .WithUpstreamTemplate("/api/products/{productId}") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("/api/products/.*/$") - .WithAuthenticationProvider("IdentityServer") - .WithAuthenticationProviderUrl("http://localhost:51888") - .WithRequireHttps(false) - .WithScopeSecret("secret") - .WithAuthenticationProviderScopeName("api") + .WithAuthenticationOptions(authenticationOptions) .Build() }; @@ -276,8 +497,8 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - UpstreamTemplate = "/api/products/{productId}", - DownstreamTemplate = "/products/{productId}", + UpstreamPathTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", ReRouteIsCaseSensitive = true, AuthenticationOptions = new FileAuthenticationOptions @@ -293,6 +514,7 @@ namespace Ocelot.UnitTests.Configuration } })) .And(x => x.GivenTheConfigIsValid()) + .And(x => x.GivenTheLoadBalancerFactoryReturns()) .When(x => x.WhenICreateTheConfig()) .Then(x => x.ThenTheReRoutesAre(expected)) .And(x => x.ThenTheAuthenticationOptionsAre(expected)) @@ -308,8 +530,8 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - UpstreamTemplate = "/api/products/{productId}/variants/{variantId}", - DownstreamTemplate = "/products/{productId}", + UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}", + DownstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", ReRouteIsCaseSensitive = true } @@ -320,8 +542,8 @@ namespace Ocelot.UnitTests.Configuration .Then(x => x.ThenTheReRoutesAre(new List { new ReRouteBuilder() - .WithDownstreamTemplate("/products/{productId}") - .WithUpstreamTemplate("/api/products/{productId}/variants/{variantId}") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}/variants/{variantId}") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("/api/products/.*/variants/.*/$") .Build() @@ -338,8 +560,8 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - UpstreamTemplate = "/api/products/{productId}/variants/{variantId}/", - DownstreamTemplate = "/products/{productId}", + UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}/", + DownstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = "Get", ReRouteIsCaseSensitive = true } @@ -350,8 +572,8 @@ namespace Ocelot.UnitTests.Configuration .Then(x => x.ThenTheReRoutesAre(new List { new ReRouteBuilder() - .WithDownstreamTemplate("/products/{productId}") - .WithUpstreamTemplate("/api/products/{productId}/variants/{variantId}/") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamPathTemplate("/api/products/{productId}/variants/{variantId}/") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("/api/products/.*/variants/.*/$") .Build() @@ -368,8 +590,8 @@ namespace Ocelot.UnitTests.Configuration { new FileReRoute { - UpstreamTemplate = "/", - DownstreamTemplate = "/api/products/", + UpstreamPathTemplate = "/", + DownstreamPathTemplate = "/api/products/", UpstreamHttpMethod = "Get", ReRouteIsCaseSensitive = true } @@ -380,10 +602,10 @@ namespace Ocelot.UnitTests.Configuration .Then(x => x.ThenTheReRoutesAre(new List { new ReRouteBuilder() - .WithDownstreamTemplate("/api/products/") - .WithUpstreamTemplate("/") + .WithDownstreamPathTemplate("/api/products/") + .WithUpstreamPathTemplate("/") .WithUpstreamHttpMethod("Get") - .WithUpstreamTemplatePattern("/$") + .WithUpstreamTemplatePattern("^/$") .Build() })) .BDDfy(); @@ -406,7 +628,7 @@ namespace Ocelot.UnitTests.Configuration private void WhenICreateTheConfig() { - _config = _ocelotConfigurationCreator.Create(); + _config = _ocelotConfigurationCreator.Create().Result; } private void ThenTheReRoutesAre(List expectedReRoutes) @@ -416,9 +638,9 @@ namespace Ocelot.UnitTests.Configuration var result = _config.Data.ReRoutes[i]; var expected = expectedReRoutes[i]; - result.DownstreamTemplate.ShouldBe(expected.DownstreamTemplate); + result.DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate.Value); result.UpstreamHttpMethod.ShouldBe(expected.UpstreamHttpMethod); - result.UpstreamTemplate.ShouldBe(expected.UpstreamTemplate); + result.UpstreamPathTemplate.Value.ShouldBe(expected.UpstreamPathTemplate.Value); result.UpstreamTemplatePattern.ShouldBe(expected.UpstreamTemplatePattern); } } @@ -439,5 +661,43 @@ namespace Ocelot.UnitTests.Configuration } } + + private void GivenTheLoadBalancerFactoryReturns() + { + _loadBalancerFactory + .Setup(x => x.Get(It.IsAny())) + .ReturnsAsync(_loadBalancer.Object); + } + + private void TheLoadBalancerFactoryIsCalledCorrectly() + { + _loadBalancerFactory + .Verify(x => x.Get(It.IsAny()), Times.Once); + } + + private void ThenTheLoadBalancerHouseIsCalledCorrectly() + { + _loadBalancerHouse + .Verify(x => x.Add(It.IsAny(), _loadBalancer.Object), Times.Once); + } + + private void GivenTheQosProviderFactoryReturns() + { + _qosProviderFactory + .Setup(x => x.Get(It.IsAny())) + .Returns(_qosProvider.Object); + } + + private void TheQosProviderFactoryIsCalledCorrectly() + { + _qosProviderFactory + .Verify(x => x.Get(It.IsAny()), Times.Once); + } + + private void ThenTheQosProviderHouseIsCalledCorrectly() + { + _qosProviderHouse + .Verify(x => x.Add(It.IsAny(), _qosProvider.Object), Times.Once); + } } } diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs index 1bdb3279..a1ea4af7 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs @@ -1,115 +1,62 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Moq; using Ocelot.Configuration; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.Provider; -using Ocelot.Configuration.Repository; -using Ocelot.Errors; +using Ocelot.Configuration.File; using Ocelot.Responses; using Shouldly; using TestStack.BDDfy; using Xunit; +using Newtonsoft.Json; +using System.IO; +using Ocelot.Configuration.Provider; +using Ocelot.Configuration.Repository; namespace Ocelot.UnitTests.Configuration { public class FileConfigurationProviderTests { - private readonly IOcelotConfigurationProvider _ocelotConfigurationProvider; - private readonly Mock _configurationRepository; - private readonly Mock _creator; - private Response _result; + private readonly IFileConfigurationProvider _provider; + private Mock _repo; + private FileConfiguration _result; + private FileConfiguration _fileConfiguration; public FileConfigurationProviderTests() { - _creator = new Mock(); - _configurationRepository = new Mock(); - _ocelotConfigurationProvider = new OcelotConfigurationProvider(_configurationRepository.Object, _creator.Object); + _repo = new Mock(); + _provider = new FileConfigurationProvider(_repo.Object); } [Fact] - public void should_get_config() + public void should_return_file_configuration() { - this.Given(x => x.GivenTheRepoReturns(new OkResponse(new OcelotConfiguration(new List())))) - .When(x => x.WhenIGetTheConfig()) - .Then(x => x.TheFollowingIsReturned(new OkResponse(new OcelotConfiguration(new List())))) + var config = new FileConfiguration(); + + this.Given(x => x.GivenTheConfigurationIs(config)) + .When(x => x.WhenIGetTheReRoutes()) + .Then(x => x.ThenTheRepoIsCalledCorrectly()) .BDDfy(); } - [Fact] - public void should_create_config_if_it_doesnt_exist() - { - this.Given(x => x.GivenTheRepoReturns(new OkResponse(null))) - .And(x => x.GivenTheCreatorReturns(new OkResponse(new OcelotConfiguration(new List())))) - .When(x => x.WhenIGetTheConfig()) - .Then(x => x.TheFollowingIsReturned(new OkResponse(new OcelotConfiguration(new List())))) - .BDDfy(); - } + - [Fact] - public void should_return_error() + private void GivenTheConfigurationIs(FileConfiguration fileConfiguration) { - this.Given(x => x.GivenTheRepoReturns(new ErrorResponse(new List - { - new AnyError() - }))) - .When(x => x.WhenIGetTheConfig()) - .Then(x => x.TheFollowingIsReturned( - new ErrorResponse(new List - { - new AnyError() - }))) - .BDDfy(); - } - - [Fact] - public void should_return_error_if_creator_errors() - { - this.Given(x => x.GivenTheRepoReturns(new OkResponse(null))) - .And(x => x.GivenTheCreatorReturns(new ErrorResponse(new List - { - new AnyError() - }))) - .When(x => x.WhenIGetTheConfig()) - .Then(x => x.TheFollowingIsReturned(new ErrorResponse(new List - { - new AnyError() - }))) - .BDDfy(); - } - - private void GivenTheCreatorReturns(Response config) - { - _creator - .Setup(x => x.Create()) - .Returns(config); - } - - private void GivenTheRepoReturns(Response config) - { - _configurationRepository + _fileConfiguration = fileConfiguration; + _repo .Setup(x => x.Get()) - .Returns(config); + .Returns(new OkResponse(fileConfiguration)); } - private void WhenIGetTheConfig() + private void WhenIGetTheReRoutes() { - _result = _ocelotConfigurationProvider.Get(); + _result = _provider.Get().Data; } - private void TheFollowingIsReturned(Response expected) + private void ThenTheRepoIsCalledCorrectly() { - _result.IsError.ShouldBe(expected.IsError); - } - - class AnyError : Error - { - public AnyError() - : base("blamo", OcelotErrorCode.UnknownError) - { - } + _repo + .Verify(x => x.Get(), Times.Once); } } -} +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationRepositoryTests.cs new file mode 100644 index 00000000..dfa8f67a --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationRepositoryTests.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.File; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; +using Newtonsoft.Json; +using System.IO; +using Ocelot.Configuration.Repository; + +namespace Ocelot.UnitTests.Configuration +{ + public class FileConfigurationRepositoryTests + { + private readonly IFileConfigurationRepository _repo; + private FileConfiguration _result; + private FileConfiguration _fileConfiguration; + + public FileConfigurationRepositoryTests() + { + _repo = new FileConfigurationRepository(); + } + + [Fact] + public void should_return_file_configuration() + { + var reRoutes = new List + { + new FileReRoute + { + DownstreamHost = "localhost", + DownstreamPort = 80, + DownstreamScheme = "https", + DownstreamPathTemplate = "/test/test/{test}" + } + }; + + var globalConfiguration = new FileGlobalConfiguration + { + AdministrationPath = "testy", + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Provider = "consul", + Port = 198, + Host = "blah" + } + }; + + var config = new FileConfiguration(); + config.GlobalConfiguration = globalConfiguration; + config.ReRoutes.AddRange(reRoutes); + + this.Given(x => x.GivenTheConfigurationIs(config)) + .When(x => x.WhenIGetTheReRoutes()) + .Then(x => x.ThenTheFollowingIsReturned(config)) + .BDDfy(); + } + + [Fact] + public void should_set_file_configuration() + { + var reRoutes = new List + { + new FileReRoute + { + DownstreamHost = "123.12.12.12", + DownstreamPort = 80, + DownstreamScheme = "https", + DownstreamPathTemplate = "/asdfs/test/{test}" + } + }; + + var globalConfiguration = new FileGlobalConfiguration + { + AdministrationPath = "asdas", + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Provider = "consul", + Port = 198, + Host = "blah" + } + }; + + var config = new FileConfiguration(); + config.GlobalConfiguration = globalConfiguration; + config.ReRoutes.AddRange(reRoutes); + + this.Given(x => GivenIHaveAConfiguration(config)) + .When(x => WhenISetTheConfiguration()) + .Then(x => ThenTheConfigurationIsStoredAs(config)) + .BDDfy(); + } + + private void GivenIHaveAConfiguration(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + + private void WhenISetTheConfiguration() + { + _repo.Set(_fileConfiguration); + _result = _repo.Get().Data; + } + + private void ThenTheConfigurationIsStoredAs(FileConfiguration expected) + { + _result.GlobalConfiguration.AdministrationPath.ShouldBe(expected.GlobalConfiguration.AdministrationPath); + _result.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Provider.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Provider); + + for(var i = 0; i < _result.ReRoutes.Count; i++) + { + _result.ReRoutes[i].DownstreamHost.ShouldBe(expected.ReRoutes[i].DownstreamHost); + _result.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expected.ReRoutes[i].DownstreamPathTemplate); + _result.ReRoutes[i].DownstreamPort.ShouldBe(expected.ReRoutes[i].DownstreamPort); + _result.ReRoutes[i].DownstreamScheme.ShouldBe(expected.ReRoutes[i].DownstreamScheme); + } + } + + private void GivenTheConfigurationIs(FileConfiguration fileConfiguration) + { + var configurationPath = $"{AppContext.BaseDirectory}/configuration.json"; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + private void WhenIGetTheReRoutes() + { + _result = _repo.Get().Data; + } + + private void ThenTheFollowingIsReturned(FileConfiguration expected) + { + _result.GlobalConfiguration.AdministrationPath.ShouldBe(expected.GlobalConfiguration.AdministrationPath); + _result.GlobalConfiguration.RequestIdKey.ShouldBe(expected.GlobalConfiguration.RequestIdKey); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Host); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Port); + _result.GlobalConfiguration.ServiceDiscoveryProvider.Provider.ShouldBe(expected.GlobalConfiguration.ServiceDiscoveryProvider.Provider); + + for(var i = 0; i < _result.ReRoutes.Count; i++) + { + _result.ReRoutes[i].DownstreamHost.ShouldBe(expected.ReRoutes[i].DownstreamHost); + _result.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expected.ReRoutes[i].DownstreamPathTemplate); + _result.ReRoutes[i].DownstreamPort.ShouldBe(expected.ReRoutes[i].DownstreamPort); + _result.ReRoutes[i].DownstreamScheme.ShouldBe(expected.ReRoutes[i].DownstreamScheme); + } + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs new file mode 100644 index 00000000..bf4804f6 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; +using Ocelot.Configuration.Setter; +using Ocelot.Errors; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class FileConfigurationSetterTests + { + private FileConfiguration _fileConfiguration; + private FileConfigurationSetter _configSetter; + private Mock _configRepo; + private Mock _configCreator; + private Response _configuration; + private object _result; + private Mock _repo; + + public FileConfigurationSetterTests() + { + _repo = new Mock(); + _configRepo = new Mock(); + _configCreator = new Mock(); + _configSetter = new FileConfigurationSetter(_configRepo.Object, _configCreator.Object, _repo.Object); + } + + [Fact] + public void should_set_configuration() + { + var fileConfig = new FileConfiguration(); + var config = new OcelotConfiguration(new List(), string.Empty); + + this.Given(x => GivenTheFollowingConfiguration(fileConfig)) + .And(x => GivenTheRepoReturns(new OkResponse())) + .And(x => GivenTheCreatorReturns(new OkResponse(config))) + .When(x => WhenISetTheConfiguration()) + .Then(x => ThenTheConfigurationRepositoryIsCalledCorrectly()) + .BDDfy(); + } + + + [Fact] + public void should_return_error_if_unable_to_set_file_configuration() + { + var fileConfig = new FileConfiguration(); + + this.Given(x => GivenTheFollowingConfiguration(fileConfig)) + .And(x => GivenTheRepoReturns(new ErrorResponse(It.IsAny()))) + .When(x => WhenISetTheConfiguration()) + .And(x => ThenAnErrorResponseIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_unable_to_set_ocelot_configuration() + { + var fileConfig = new FileConfiguration(); + + this.Given(x => GivenTheFollowingConfiguration(fileConfig)) + .And(x => GivenTheRepoReturns(new OkResponse())) + .And(x => GivenTheCreatorReturns(new ErrorResponse(It.IsAny()))) + .When(x => WhenISetTheConfiguration()) + .And(x => ThenAnErrorResponseIsReturned()) + .BDDfy(); + } + + private void GivenTheRepoReturns(Response response) + { + _repo + .Setup(x => x.Set(It.IsAny())) + .Returns(response); + } + + private void ThenAnErrorResponseIsReturned() + { + _result.ShouldBeOfType(); + } + + private void GivenTheCreatorReturns(Response configuration) + { + _configuration = configuration; + _configCreator + .Setup(x => x.Create(_fileConfiguration)) + .ReturnsAsync(_configuration); + } + + private void GivenTheFollowingConfiguration(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + + private void WhenISetTheConfiguration() + { + _result = _configSetter.Set(_fileConfiguration).Result; + } + + private void ThenTheConfigurationRepositoryIsCalledCorrectly() + { + _configRepo + .Verify(x => x.AddOrReplace(_configuration.Data), Times.Once); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/HashCreationTests.cs b/test/Ocelot.UnitTests/Configuration/HashCreationTests.cs new file mode 100644 index 00000000..d99efd58 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/HashCreationTests.cs @@ -0,0 +1,33 @@ +using System; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using Shouldly; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class HashCreationTests + { + [Fact] + public void should_create_hash_and_salt() + { + var password = "secret"; + + var salt = new byte[128 / 8]; + + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(salt); + } + + var storedSalt = Convert.ToBase64String(salt); + + var storedHash = Convert.ToBase64String(KeyDerivation.Pbkdf2( + password: password, + salt: salt, + prf: KeyDerivationPrf.HMACSHA256, + iterationCount: 10000, + numBytesRequested: 256 / 8)); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/HashMatcherTests.cs b/test/Ocelot.UnitTests/Configuration/HashMatcherTests.cs new file mode 100644 index 00000000..34e9477e --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/HashMatcherTests.cs @@ -0,0 +1,76 @@ +using Ocelot.Configuration.Authentication; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class HashMatcherTests + { + private string _password; + private string _hash; + private string _salt; + private bool _result; + private HashMatcher _hashMatcher; + + public HashMatcherTests() + { + _hashMatcher = new HashMatcher(); + } + + [Fact] + public void should_match_hash() + { + var hash = "kE/mxd1hO9h9Sl2VhGhwJUd9xZEv4NP6qXoN39nIqM4="; + var salt = "zzWITpnDximUNKYLiUam/w=="; + var password = "secret"; + + this.Given(x => GivenThePassword(password)) + .And(x => GivenTheHash(hash)) + .And(x => GivenTheSalt(salt)) + .When(x => WhenIMatch()) + .Then(x => ThenTheResultIs(true)) + .BDDfy(); + } + + [Fact] + public void should_not_match_hash() + { + var hash = "kE/mxd1hO9h9Sl2VhGhwJUd9xZEv4NP6qXoN39nIqM4="; + var salt = "zzWITpnDximUNKYLiUam/w=="; + var password = "secret1"; + + this.Given(x => GivenThePassword(password)) + .And(x => GivenTheHash(hash)) + .And(x => GivenTheSalt(salt)) + .When(x => WhenIMatch()) + .Then(x => ThenTheResultIs(false)) + .BDDfy(); + } + + private void GivenThePassword(string password) + { + _password = password; + } + + private void GivenTheHash(string hash) + { + _hash = hash; + } + + private void GivenTheSalt(string salt) + { + _salt = salt; + } + + private void WhenIMatch() + { + _result = _hashMatcher.Match(_password, _salt, _hash); + } + + private void ThenTheResultIs(bool expected) + { + _result.ShouldBe(expected); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs index 8f7af24e..8d28b112 100644 --- a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs @@ -27,7 +27,7 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void can_add_config() { - this.Given(x => x.GivenTheConfigurationIs(new FakeConfig("initial"))) + this.Given(x => x.GivenTheConfigurationIs(new FakeConfig("initial", "adminath"))) .When(x => x.WhenIAddOrReplaceTheConfig()) .Then(x => x.ThenNoErrorsAreReturned()) .BDDfy(); @@ -44,7 +44,7 @@ namespace Ocelot.UnitTests.Configuration private void ThenTheConfigurationIsReturned() { - _getResult.Data.ReRoutes[0].DownstreamTemplate.ShouldBe("initial"); + _getResult.Data.ReRoutes[0].DownstreamPathTemplate.Value.ShouldBe("initial"); } private void WhenIGetTheConfiguration() @@ -54,7 +54,7 @@ namespace Ocelot.UnitTests.Configuration private void GivenThereIsASavedConfiguration() { - GivenTheConfigurationIs(new FakeConfig("initial")); + GivenTheConfigurationIs(new FakeConfig("initial", "adminath")); WhenIAddOrReplaceTheConfig(); } @@ -75,17 +75,23 @@ namespace Ocelot.UnitTests.Configuration class FakeConfig : IOcelotConfiguration { - private readonly string _downstreamTemplate; + private readonly string _downstreamTemplatePath; - public FakeConfig(string downstreamTemplate) + public FakeConfig(string downstreamTemplatePath, string administrationPath) { - _downstreamTemplate = downstreamTemplate; + _downstreamTemplatePath = downstreamTemplatePath; + AdministrationPath = administrationPath; } public List ReRoutes => new List { - new ReRouteBuilder().WithDownstreamTemplate(_downstreamTemplate).Build() + new ReRouteBuilder() + .WithDownstreamPathTemplate(_downstreamTemplatePath) + .WithUpstreamHttpMethod("Get") + .Build() }; + + public string AdministrationPath {get;} } } } diff --git a/test/Ocelot.UnitTests/Configuration/OcelotConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Configuration/OcelotConfigurationProviderTests.cs new file mode 100644 index 00000000..9be55087 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/OcelotConfigurationProviderTests.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.Provider; +using Ocelot.Configuration.Repository; +using Ocelot.Errors; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class OcelotConfigurationProviderTests + { + private readonly IOcelotConfigurationProvider _ocelotConfigurationProvider; + private readonly Mock _configurationRepository; + private Response _result; + + public OcelotConfigurationProviderTests() + { + _configurationRepository = new Mock(); + _ocelotConfigurationProvider = new OcelotConfigurationProvider(_configurationRepository.Object); + } + + [Fact] + public void should_get_config() + { + this.Given(x => x.GivenTheRepoReturns(new OkResponse(new OcelotConfiguration(new List(), string.Empty)))) + .When(x => x.WhenIGetTheConfig()) + .Then(x => x.TheFollowingIsReturned(new OkResponse(new OcelotConfiguration(new List(), string.Empty)))) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + this.Given(x => x.GivenTheRepoReturns(new ErrorResponse(new List + { + new AnyError() + }))) + .When(x => x.WhenIGetTheConfig()) + .Then(x => x.TheFollowingIsReturned( + new ErrorResponse(new List + { + new AnyError() + }))) + .BDDfy(); + } + + private void GivenTheRepoReturns(Response config) + { + _configurationRepository + .Setup(x => x.Get()) + .Returns(config); + } + + private void WhenIGetTheConfig() + { + _result = _ocelotConfigurationProvider.Get(); + } + + private void TheFollowingIsReturned(Response expected) + { + _result.IsError.ShouldBe(expected.IsError); + } + + class AnyError : Error + { + public AnyError() + : base("blamo", OcelotErrorCode.UnknownError) + { + } + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/OcelotResourceOwnerPasswordValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/OcelotResourceOwnerPasswordValidatorTests.cs new file mode 100644 index 00000000..a8d11713 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/OcelotResourceOwnerPasswordValidatorTests.cs @@ -0,0 +1,117 @@ +using Ocelot.Configuration.Authentication; +using Xunit; +using Shouldly; +using TestStack.BDDfy; +using Moq; +using IdentityServer4.Validation; +using Ocelot.Configuration.Provider; +using System.Collections.Generic; + +namespace Ocelot.UnitTests.Configuration +{ + public class OcelotResourceOwnerPasswordValidatorTests + { + private OcelotResourceOwnerPasswordValidator _validator; + private Mock _matcher; + private string _userName; + private string _password; + private ResourceOwnerPasswordValidationContext _context; + private Mock _config; + private User _user; + + public OcelotResourceOwnerPasswordValidatorTests() + { + _matcher = new Mock(); + _config = new Mock(); + _validator = new OcelotResourceOwnerPasswordValidator(_matcher.Object, _config.Object); + } + + [Fact] + public void should_return_success() + { + this.Given(x => GivenTheUserName("tom")) + .And(x => GivenThePassword("password")) + .And(x => GivenTheUserIs(new User("sub", "tom", "xxx", "xxx"))) + .And(x => GivenTheMatcherReturns(true)) + .When(x => WhenIValidate()) + .Then(x => ThenTheUserIsValidated()) + .And(x => ThenTheMatcherIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_fail_when_no_user() + { + this.Given(x => GivenTheUserName("bob")) + .And(x => GivenTheUserIs(new User("sub", "tom", "xxx", "xxx"))) + .And(x => GivenTheMatcherReturns(true)) + .When(x => WhenIValidate()) + .Then(x => ThenTheUserIsNotValidated()) + .BDDfy(); + } + + [Fact] + public void should_return_fail_when_password_doesnt_match() + { + this.Given(x => GivenTheUserName("tom")) + .And(x => GivenThePassword("password")) + .And(x => GivenTheUserIs(new User("sub", "tom", "xxx", "xxx"))) + .And(x => GivenTheMatcherReturns(false)) + .When(x => WhenIValidate()) + .Then(x => ThenTheUserIsNotValidated()) + .And(x => ThenTheMatcherIsCalledCorrectly()) + .BDDfy(); + } + + private void ThenTheMatcherIsCalledCorrectly() + { + _matcher + .Verify(x => x.Match(_password, _user.Salt, _user.Hash), Times.Once); + } + + private void GivenThePassword(string password) + { + _password = password; + } + + private void GivenTheUserIs(User user) + { + _user = user; + _config + .Setup(x => x.Users) + .Returns(new List{_user}); + } + + private void GivenTheMatcherReturns(bool expected) + { + _matcher + .Setup(x => x.Match(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(expected); + } + + private void GivenTheUserName(string userName) + { + _userName = userName; + } + + private void WhenIValidate() + { + _context = new ResourceOwnerPasswordValidationContext + { + UserName = _userName, + Password = _password + }; + _validator.ValidateAsync(_context).Wait(); + } + + private void ThenTheUserIsValidated() + { + _context.Result.IsError.ShouldBe(false); + } + + private void ThenTheUserIsNotValidated() + { + _context.Result.IsError.ShouldBe(true); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs new file mode 100644 index 00000000..7f4d7b87 --- /dev/null +++ b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs @@ -0,0 +1,133 @@ +using System; +using System.Net; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Setter; +using Ocelot.Controllers; +using Ocelot.Errors; +using Ocelot.Responses; +using TestStack.BDDfy; +using Xunit; +using Shouldly; +using Ocelot.Configuration.Provider; + +namespace Ocelot.UnitTests.Controllers +{ + public class FileConfigurationControllerTests + { + private FileConfigurationController _controller; + private Mock _configGetter; + private Mock _configSetter; + private IActionResult _result; + private FileConfiguration _fileConfiguration; + + public FileConfigurationControllerTests() + { + _configGetter = new Mock(); + _configSetter = new Mock(); + _controller = new FileConfigurationController(_configGetter.Object, _configSetter.Object); + } + + [Fact] + public void should_get_file_configuration() + { + var expected = new OkResponse(new FileConfiguration()); + + this.Given(x => x.GivenTheGetConfigurationReturns(expected)) + .When(x => x.WhenIGetTheFileConfiguration()) + .Then(x => x.TheTheGetFileConfigurationIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_error_when_cannot_get_config() + { + var expected = new ErrorResponse(It.IsAny()); + + this.Given(x => x.GivenTheGetConfigurationReturns(expected)) + .When(x => x.WhenIGetTheFileConfiguration()) + .Then(x => x.TheTheGetFileConfigurationIsCalledCorrectly()) + .And(x => x.ThenTheResponseIs()) + .BDDfy(); + } + + [Fact] + public void should_post_file_configuration() + { + var expected = new FileConfiguration(); + + this.Given(x => GivenTheFileConfiguration(expected)) + .And(x => GivenTheConfigSetterReturnsAnError(new OkResponse())) + .When(x => WhenIPostTheFileConfiguration()) + .Then(x => x.ThenTheConfigrationSetterIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_error_when_cannot_set_config() + { + var expected = new FileConfiguration(); + + this.Given(x => GivenTheFileConfiguration(expected)) + .And(x => GivenTheConfigSetterReturnsAnError(new ErrorResponse(new FakeError()))) + .When(x => WhenIPostTheFileConfiguration()) + .Then(x => x.ThenTheConfigrationSetterIsCalledCorrectly()) + .And(x => ThenTheResponseIs()) + .BDDfy(); + } + + private void GivenTheConfigSetterReturnsAnError(Response response) + { + _configSetter + .Setup(x => x.Set(It.IsAny())) + .ReturnsAsync(response); + } + + private void ThenTheConfigrationSetterIsCalledCorrectly() + { + _configSetter + .Verify(x => x.Set(_fileConfiguration), Times.Once); + } + + private void WhenIPostTheFileConfiguration() + { + _result = _controller.Post(_fileConfiguration).Result; + } + + private void GivenTheFileConfiguration(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + + private void ThenTheResponseIs() + { + _result.ShouldBeOfType(); + } + + private void GivenTheGetConfigurationReturns(Response fileConfiguration) + { + _configGetter + .Setup(x => x.Get()) + .Returns(fileConfiguration); + } + + private void WhenIGetTheFileConfiguration() + { + _result = _controller.Get(); + } + + private void TheTheGetFileConfigurationIsCalledCorrectly() + { + _configGetter + .Verify(x => x.Get(), Times.Once); + } + + class FakeError : Error + { + public FakeError() : base(string.Empty, OcelotErrorCode.CannotAddDataError) + { + } + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index 22dfea80..90a35ad8 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -61,7 +61,13 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder [Fact] public void should_call_scoped_data_repository_correctly() { - this.Given(x => x.GivenTheDownStreamRouteFinderReturns(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamTemplate("any old string").Build()))) + this.Given(x => x.GivenTheDownStreamRouteFinderReturns( + new DownstreamRoute( + new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) .BDDfy(); diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index bb390d32..49e9b01b 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -35,6 +35,38 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder [Fact] public void should_return_route() + { + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("matchInUrlMatcher")) + .And(x =>x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>( + new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("someUpstreamPath") + .Build() + }, string.Empty + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRoute( + new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod("Get") + .Build() + ))) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_route_if_upstream_path_and_upstream_template_are_the_same() { this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) .And( @@ -44,12 +76,12 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() - .WithDownstreamTemplate("someDownstreamPath") - .WithUpstreamTemplate("someUpstreamPath") + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("someUpstreamPath") .Build() - } + }, string.Empty )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) @@ -57,10 +89,11 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), new ReRouteBuilder() - .WithDownstreamTemplate("someDownstreamPath") + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamHttpMethod("Get") .Build() ))) - .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .And(x => x.ThenTheUrlMatcherIsNotCalled()) .BDDfy(); } @@ -75,18 +108,18 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() - .WithDownstreamTemplate("someDownstreamPath") - .WithUpstreamTemplate("someUpstreamPath") + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("") .Build(), new ReRouteBuilder() - .WithDownstreamTemplate("someDownstreamPathForAPost") - .WithUpstreamTemplate("someUpstreamPath") + .WithDownstreamPathTemplate("someDownstreamPathForAPost") + .WithUpstreamPathTemplate("someUpstreamPath") .WithUpstreamHttpMethod("Post") .WithUpstreamTemplatePattern("") .Build() - } + }, string.Empty )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) @@ -94,7 +127,8 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .Then( x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), new ReRouteBuilder() - .WithDownstreamTemplate("someDownstreamPathForAPost") + .WithDownstreamPathTemplate("someDownstreamPathForAPost") + .WithUpstreamHttpMethod("Post") .Build() ))) .BDDfy(); @@ -103,16 +137,16 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder [Fact] public void should_not_return_route() { - this.Given(x => x.GivenThereIsAnUpstreamUrlPath("somePath")) + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("dontMatchPath")) .And(x => x.GivenTheConfigurationIs(new List { new ReRouteBuilder() - .WithDownstreamTemplate("somPath") - .WithUpstreamTemplate("somePath") + .WithDownstreamPathTemplate("somPath") + .WithUpstreamPathTemplate("somePath") .WithUpstreamHttpMethod("Get") .WithUpstreamTemplatePattern("somePath") .Build(), - } + }, string.Empty )) .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(false)))) .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) @@ -143,7 +177,13 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder private void ThenTheUrlMatcherIsCalledCorrectly() { _mockMatcher - .Verify(x => x.Match(_upstreamUrlPath, _reRoutesConfig[0].UpstreamTemplate), Times.Once); + .Verify(x => x.Match(_upstreamUrlPath, _reRoutesConfig[0].UpstreamPathTemplate.Value), Times.Once); + } + + private void ThenTheUrlMatcherIsNotCalled() + { + _mockMatcher + .Verify(x => x.Match(_upstreamUrlPath, _reRoutesConfig[0].UpstreamPathTemplate.Value), Times.Never); } private void GivenTheUrlMatcherReturns(Response match) @@ -154,12 +194,12 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder .Returns(_match); } - private void GivenTheConfigurationIs(List reRoutesConfig) + private void GivenTheConfigurationIs(List reRoutesConfig, string adminPath) { _reRoutesConfig = reRoutesConfig; _mockConfig .Setup(x => x.Get()) - .Returns(new OkResponse(new OcelotConfiguration(_reRoutesConfig))); + .Returns(new OkResponse(new OcelotConfiguration(_reRoutesConfig, adminPath))); } private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) @@ -174,7 +214,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder private void ThenTheFollowingIsReturned(DownstreamRoute expected) { - _result.Data.ReRoute.DownstreamTemplate.ShouldBe(expected.ReRoute.DownstreamTemplate); + _result.Data.ReRoute.DownstreamPathTemplate.Value.ShouldBe(expected.ReRoute.DownstreamPathTemplate.Value); for (int i = 0; i < _result.Data.TemplatePlaceholderNameAndValues.Count; i++) { diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs index ea069593..70dd747d 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -18,6 +18,26 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher _urlMatcher = new RegExUrlMatcher(); } + [Fact] + public void should_not_match_forward_slash_only_regex() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/working/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + [Fact] + public void should_match_forward_slash_only_regex() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("^/$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + [Fact] public void should_find_match_when_template_smaller_than_valid_path() { diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index 98bc5f0b..438710f6 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -7,15 +7,18 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; +using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.Middleware; using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator; using Ocelot.DownstreamUrlCreator.Middleware; using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Responses; +using Ocelot.Values; using TestStack.BDDfy; using Xunit; @@ -23,21 +26,24 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator { public class DownstreamUrlCreatorMiddlewareTests : IDisposable { - private readonly Mock _downstreamUrlTemplateVariableReplacer; + private readonly Mock _downstreamUrlTemplateVariableReplacer; private readonly Mock _scopedRepository; + private readonly Mock _urlBuilder; private readonly string _url; private readonly TestServer _server; private readonly HttpClient _client; private Response _downstreamRoute; private HttpResponseMessage _result; + private OkResponse _downstreamPath; private OkResponse _downstreamUrl; + private HostAndPort _hostAndPort; public DownstreamUrlCreatorMiddlewareTests() { _url = "http://localhost:51879"; - _downstreamUrlTemplateVariableReplacer = new Mock(); + _downstreamUrlTemplateVariableReplacer = new Mock(); _scopedRepository = new Mock(); - + _urlBuilder = new Mock(); var builder = new WebHostBuilder() .ConfigureServices(x => { @@ -45,6 +51,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator x.AddLogging(); x.AddSingleton(_downstreamUrlTemplateVariableReplacer.Object); x.AddSingleton(_scopedRepository.Object); + x.AddSingleton(_urlBuilder.Object); }) .UseUrls(_url) .UseKestrel() @@ -61,21 +68,47 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator } [Fact] - public void should_call_scoped_data_repository_correctly() + public void should_call_dependencies_correctly() { - this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamTemplate("any old string").Build()))) - .And(x => x.TheUrlReplacerReturns("any old string")) + var hostAndPort = new HostAndPort("127.0.0.1", 80); + + this.Given(x => x.GivenTheDownStreamRouteIs( + new DownstreamRoute( + new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithUpstreamHttpMethod("Get") + .Build()))) + .And(x => x.GivenTheHostAndPortIs(hostAndPort)) + .And(x => x.TheUrlReplacerReturns("/api/products/1")) + .And(x => x.TheUrlBuilderReturns("http://127.0.0.1:80/api/products/1")) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) .BDDfy(); } + private void GivenTheHostAndPortIs(HostAndPort hostAndPort) + { + _hostAndPort = hostAndPort; + _scopedRepository + .Setup(x => x.Get("HostAndPort")) + .Returns(new OkResponse(_hostAndPort)); + } + + private void TheUrlBuilderReturns(string dsUrl) + { + _downstreamUrl = new OkResponse(new DownstreamUrl(dsUrl)); + _urlBuilder + .Setup(x => x.Build(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_downstreamUrl); + } + private void TheUrlReplacerReturns(string downstreamUrl) { - _downstreamUrl = new OkResponse(new DownstreamUrl(downstreamUrl)); + _downstreamPath = new OkResponse(new DownstreamPath(downstreamUrl)); _downstreamUrlTemplateVariableReplacer - .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) - .Returns(_downstreamUrl); + .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) + .Returns(_downstreamPath); } private void ThenTheScopedDataRepositoryIsCalledCorrectly() diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlBuilderTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlBuilderTests.cs new file mode 100644 index 00000000..7e512798 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlBuilderTests.cs @@ -0,0 +1,124 @@ +using System; +using Ocelot.Configuration; +using Ocelot.DownstreamUrlCreator; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamUrlCreator +{ + public class UrlBuilderTests + { + private readonly IUrlBuilder _urlBuilder; + private string _dsPath; + private string _dsScheme; + private string _dsHost; + private int _dsPort; + + private Response _result; + + public UrlBuilderTests() + { + _urlBuilder = new UrlBuilder(); + } + + [Fact] + public void should_return_error_when_downstream_path_is_null() + { + this.Given(x => x.GivenADownstreamPath(null)) + .When(x => x.WhenIBuildTheUrl()) + .Then(x => x.ThenThereIsAnErrorOfType()) + .BDDfy(); + } + + [Fact] + public void should_return_error_when_downstream_scheme_is_null() + { + this.Given(x => x.GivenADownstreamScheme(null)) + .And(x => x.GivenADownstreamPath("test")) + .When(x => x.WhenIBuildTheUrl()) + .Then(x => x.ThenThereIsAnErrorOfType()) + .BDDfy(); + } + + [Fact] + public void should_return_error_when_downstream_host_is_null() + { + this.Given(x => x.GivenADownstreamScheme(null)) + .And(x => x.GivenADownstreamPath("test")) + .And(x => x.GivenADownstreamScheme("test")) + .When(x => x.WhenIBuildTheUrl()) + .Then(x => x.ThenThereIsAnErrorOfType()) + .BDDfy(); + } + + [Fact] + public void should_not_use_port_if_zero() + { + this.Given(x => x.GivenADownstreamPath("/api/products/1")) + .And(x => x.GivenADownstreamScheme("http")) + .And(x => x.GivenADownstreamHost("127.0.0.1")) + .And(x => x.GivenADownstreamPort(0)) + .When(x => x.WhenIBuildTheUrl()) + .Then(x => x.ThenTheUrlIsReturned("http://127.0.0.1/api/products/1")) + .And(x => x.ThenTheUrlIsWellFormed()) + .BDDfy(); + } + + [Fact] + public void should_build_well_formed_uri() + { + this.Given(x => x.GivenADownstreamPath("/api/products/1")) + .And(x => x.GivenADownstreamScheme("http")) + .And(x => x.GivenADownstreamHost("127.0.0.1")) + .And(x => x.GivenADownstreamPort(5000)) + .When(x => x.WhenIBuildTheUrl()) + .Then(x => x.ThenTheUrlIsReturned("http://127.0.0.1:5000/api/products/1")) + .And(x => x.ThenTheUrlIsWellFormed()) + .BDDfy(); + } + + private void ThenThereIsAnErrorOfType() + { + _result.Errors[0].ShouldBeOfType(); + } + + private void GivenADownstreamPath(string dsPath) + { + _dsPath = dsPath; + } + + private void GivenADownstreamScheme(string dsScheme) + { + _dsScheme = dsScheme; + } + + private void GivenADownstreamHost(string dsHost) + { + _dsHost = dsHost; + } + + private void GivenADownstreamPort(int dsPort) + { + _dsPort = dsPort; + } + + private void WhenIBuildTheUrl() + { + _result = _urlBuilder.Build(_dsPath, _dsScheme, new HostAndPort(_dsHost, _dsPort)); + } + + private void ThenTheUrlIsReturned(string expected) + { + _result.Data.Value.ShouldBe(expected); + } + + private void ThenTheUrlIsWellFormed() + { + Uri.IsWellFormedUriString(_result.Data.Value, UriKind.Absolute).ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs index b1ad369b..00445ee6 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs @@ -4,6 +4,7 @@ using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; using Ocelot.Responses; +using Ocelot.Values; using Shouldly; using TestStack.BDDfy; using Xunit; @@ -13,18 +14,23 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer public class UpstreamUrlPathTemplateVariableReplacerTests { private DownstreamRoute _downstreamRoute; - private Response _result; - private readonly IDownstreamUrlPathPlaceholderReplacer _downstreamUrlPathReplacer; + private Response _result; + private readonly IDownstreamPathPlaceholderReplacer _downstreamPathReplacer; public UpstreamUrlPathTemplateVariableReplacerTests() { - _downstreamUrlPathReplacer = new DownstreamUrlPathPlaceholderReplacer(); + _downstreamPathReplacer = new DownstreamTemplatePathPlaceholderReplacer(); } [Fact] public void can_replace_no_template_variables() { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder().Build()))) + this.Given(x => x.GivenThereIsAUrlMatch( + new DownstreamRoute( + new List(), + new ReRouteBuilder() + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("")) .BDDfy(); @@ -33,7 +39,13 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_no_template_variables_with_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamTemplate("/").Build()))) + this.Given(x => x.GivenThereIsAUrlMatch( + new DownstreamRoute( + new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("/") + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("/")) .BDDfy(); @@ -42,7 +54,11 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_url_no_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamTemplate("api").Build()))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("api") + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api")) .BDDfy(); @@ -51,7 +67,11 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_url_one_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamTemplate("api/").Build()))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("api/") + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api/")) .BDDfy(); @@ -60,7 +80,11 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer [Fact] public void can_replace_url_multiple_slash() { - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamTemplate("api/product/products/").Build()))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("api/product/products/") + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api/product/products/")) .BDDfy(); @@ -74,7 +98,11 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer new UrlPathPlaceholderNameAndValue("{productId}", "1") }; - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, new ReRouteBuilder().WithDownstreamTemplate("productservice/products/{productId}/").Build()))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, + new ReRouteBuilder() + .WithDownstreamPathTemplate("productservice/products/{productId}/") + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/")) .BDDfy(); @@ -88,7 +116,11 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer new UrlPathPlaceholderNameAndValue("{productId}", "1") }; - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, new ReRouteBuilder().WithDownstreamTemplate("productservice/products/{productId}/variants").Build()))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, + new ReRouteBuilder() + .WithDownstreamPathTemplate("productservice/products/{productId}/variants") + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/variants")) .BDDfy(); @@ -103,7 +135,11 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer new UrlPathPlaceholderNameAndValue("{variantId}", "12") }; - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, new ReRouteBuilder().WithDownstreamTemplate("productservice/products/{productId}/variants/{variantId}").Build()))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, + new ReRouteBuilder() + .WithDownstreamPathTemplate("productservice/products/{productId}/variants/{variantId}") + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/variants/12")) .BDDfy(); @@ -119,7 +155,11 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer new UrlPathPlaceholderNameAndValue("{categoryId}", "34") }; - this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, new ReRouteBuilder().WithDownstreamTemplate("productservice/category/{categoryId}/products/{productId}/variants/{variantId}").Build()))) + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, + new ReRouteBuilder() + .WithDownstreamPathTemplate("productservice/category/{categoryId}/products/{productId}/variants/{variantId}") + .WithUpstreamHttpMethod("Get") + .Build()))) .When(x => x.WhenIReplaceTheTemplateVariables()) .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/category/34/products/1/variants/12")) .BDDfy(); @@ -132,7 +172,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer private void WhenIReplaceTheTemplateVariables() { - _result = _downstreamUrlPathReplacer.Replace(_downstreamRoute.ReRoute.DownstreamTemplate, _downstreamRoute.TemplatePlaceholderNameAndValues); + _result = _downstreamPathReplacer.Replace(_downstreamRoute.ReRoute.DownstreamPathTemplate, _downstreamRoute.TemplatePlaceholderNameAndValues); } private void ThenTheDownstreamUrlPathIsReturned(string expected) diff --git a/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs index b85802af..032a76e9 100644 --- a/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs @@ -67,11 +67,12 @@ namespace Ocelot.UnitTests.Headers { var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() - .WithDownstreamTemplate("any old string") + .WithDownstreamPathTemplate("any old string") .WithClaimsToHeaders(new List { new ClaimToThing("UserId", "Subject", "", 0) }) + .WithUpstreamHttpMethod("Get") .Build()); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) diff --git a/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs new file mode 100644 index 00000000..07002ce3 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LeastConnectionTests + { + private HostAndPort _hostAndPort; + private Response _result; + private LeastConnectionLoadBalancer _leastConnection; + private List _services; + private Random _random; + + public LeastConnectionTests() + { + _random = new Random(); + } + + [Fact] + public void should_be_able_to_lease_and_release_concurrently() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new HostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _services = availableServices; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + + var tasks = new Task[100]; + + for(var i = 0; i < tasks.Length; i++) + { + tasks[i] = LeaseDelayAndRelease(); + } + + Task.WaitAll(tasks); + } + + private async Task LeaseDelayAndRelease() + { + var hostAndPort = await _leastConnection.Lease(); + await Task.Delay(_random.Next(1, 100)); + _leastConnection.Release(hostAndPort.Data); + } + + [Fact] + public void should_get_next_url() + { + var serviceName = "products"; + + var hostAndPort = new HostAndPort("localhost", 80); + + var availableServices = new List + { + new Service(serviceName, hostAndPort, string.Empty, string.Empty, new string[0]) + }; + + this.Given(x => x.GivenAHostAndPort(hostAndPort)) + .And(x => x.GivenTheLoadBalancerStarts(availableServices, serviceName)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenTheNextHostAndPortIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_serve_from_service_with_least_connections() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new HostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.3", 80), string.Empty, string.Empty, new string[0]) + }; + + _services = availableServices; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + + var response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[2].HostAndPort.DownstreamHost); + } + + [Fact] + public void should_build_connections_per_service() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new HostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _services = availableServices; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + + var response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + } + + [Fact] + public void should_release_connection() + { + var serviceName = "products"; + + var availableServices = new List + { + new Service(serviceName, new HostAndPort("127.0.0.1", 80), string.Empty, string.Empty, new string[0]), + new Service(serviceName, new HostAndPort("127.0.0.2", 80), string.Empty, string.Empty, new string[0]), + }; + + _services = availableServices; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + + var response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + + //release this so 2 should have 1 connection and we should get 2 back as our next host and port + _leastConnection.Release(availableServices[1].HostAndPort); + + response = _leastConnection.Lease().Result; + + response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); + } + + [Fact] + public void should_return_error_if_services_are_null() + { + var serviceName = "products"; + + var hostAndPort = new HostAndPort("localhost", 80); + this.Given(x => x.GivenAHostAndPort(hostAndPort)) + .And(x => x.GivenTheLoadBalancerStarts(null, serviceName)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenServiceAreNullErrorIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_services_are_empty() + { + var serviceName = "products"; + + var hostAndPort = new HostAndPort("localhost", 80); + this.Given(x => x.GivenAHostAndPort(hostAndPort)) + .And(x => x.GivenTheLoadBalancerStarts(new List(), serviceName)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenServiceAreEmptyErrorIsReturned()) + .BDDfy(); + } + + private void ThenServiceAreNullErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + _result.Errors[0].ShouldBeOfType(); + } + + private void ThenServiceAreEmptyErrorIsReturned() + { + _result.IsError.ShouldBeTrue(); + _result.Errors[0].ShouldBeOfType(); + } + + private void GivenTheLoadBalancerStarts(List services, string serviceName) + { + _services = services; + _leastConnection = new LeastConnectionLoadBalancer(() => Task.FromResult(_services), serviceName); + } + + private void WhenTheLoadBalancerStarts(List services, string serviceName) + { + GivenTheLoadBalancerStarts(services, serviceName); + } + + private void GivenAHostAndPort(HostAndPort hostAndPort) + { + _hostAndPort = hostAndPort; + } + + private void WhenIGetTheNextHostAndPort() + { + _result = _leastConnection.Lease().Result; + } + + private void ThenTheNextHostAndPortIsReturned() + { + _result.Data.DownstreamHost.ShouldBe(_hostAndPort.DownstreamHost); + _result.Data.DownstreamPort.ShouldBe(_hostAndPort.DownstreamPort); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs new file mode 100644 index 00000000..767b4272 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs @@ -0,0 +1,118 @@ +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.ServiceDiscovery; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LoadBalancerFactoryTests + { + private ReRoute _reRoute; + private LoadBalancerFactory _factory; + private ILoadBalancer _result; + private Mock _serviceProviderFactory; + private Mock _serviceProvider; + + public LoadBalancerFactoryTests() + { + _serviceProviderFactory = new Mock(); + _serviceProvider = new Mock(); + _factory = new LoadBalancerFactory(_serviceProviderFactory.Object); + } + + [Fact] + public void should_return_no_load_balancer() + { + var reRoute = new ReRouteBuilder() + .WithServiceProviderConfiguraion(new ServiceProviderConfiguraionBuilder().Build()) + .WithUpstreamHttpMethod("Get") + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_round_robin_load_balancer() + { + var reRoute = new ReRouteBuilder() + .WithLoadBalancer("RoundRobin") + .WithUpstreamHttpMethod("Get") + .WithServiceProviderConfiguraion(new ServiceProviderConfiguraionBuilder().Build()) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_round_least_connection_balancer() + { + var reRoute = new ReRouteBuilder() + .WithLoadBalancer("LeastConnection") + .WithUpstreamHttpMethod("Get") + .WithServiceProviderConfiguraion(new ServiceProviderConfiguraionBuilder().Build()) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheLoadBalancerIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_call_service_provider() + { + var reRoute = new ReRouteBuilder() + .WithLoadBalancer("RoundRobin") + .WithUpstreamHttpMethod("Get") + .WithServiceProviderConfiguraion(new ServiceProviderConfiguraionBuilder().Build()) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .And(x => x.GivenTheServiceProviderFactoryReturns()) + .When(x => x.WhenIGetTheLoadBalancer()) + .Then(x => x.ThenTheServiceProviderIsCalledCorrectly()) + .BDDfy(); + } + + private void GivenTheServiceProviderFactoryReturns() + { + _serviceProviderFactory + .Setup(x => x.Get(It.IsAny())) + .Returns(_serviceProvider.Object); + } + + private void ThenTheServiceProviderIsCalledCorrectly() + { + _serviceProviderFactory + .Verify(x => x.Get(It.IsAny()), Times.Once); + } + + private void GivenAReRoute(ReRoute reRoute) + { + _reRoute = reRoute; + } + + private void WhenIGetTheLoadBalancer() + { + _result = _factory.Get(_reRoute).Result; + } + + private void ThenTheLoadBalancerIsReturned() + { + _result.ShouldBeOfType(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs new file mode 100644 index 00000000..ac24b490 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -0,0 +1,136 @@ +using System; +using System.Threading.Tasks; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LoadBalancerHouseTests + { + private ILoadBalancer _loadBalancer; + private readonly LoadBalancerHouse _loadBalancerHouse; + private Response _addResult; + private Response _getResult; + private string _key; + + public LoadBalancerHouseTests() + { + _loadBalancerHouse = new LoadBalancerHouse(); + } + + [Fact] + public void should_store_load_balancer() + { + var key = "test"; + + this.Given(x => x.GivenThereIsALoadBalancer(key, new FakeLoadBalancer())) + .When(x => x.WhenIAddTheLoadBalancer()) + .Then(x => x.ThenItIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_get_load_balancer() + { + var key = "test"; + + this.Given(x => x.GivenThereIsALoadBalancer(key, new FakeLoadBalancer())) + .When(x => x.WhenWeGetTheLoadBalancer(key)) + .Then(x => x.ThenItIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_store_load_balancers_by_key() + { + var key = "test"; + var keyTwo = "testTwo"; + + this.Given(x => x.GivenThereIsALoadBalancer(key, new FakeLoadBalancer())) + .And(x => x.GivenThereIsALoadBalancer(keyTwo, new FakeRoundRobinLoadBalancer())) + .When(x => x.WhenWeGetTheLoadBalancer(key)) + .Then(x => x.ThenTheLoadBalancerIs()) + .When(x => x.WhenWeGetTheLoadBalancer(keyTwo)) + .Then(x => x.ThenTheLoadBalancerIs()) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_no_load_balancer_with_key() + { + this.When(x => x.WhenWeGetTheLoadBalancer("test")) + .Then(x => x.ThenAnErrorIsReturned()) + .BDDfy(); + } + + private void ThenAnErrorIsReturned() + { + _getResult.IsError.ShouldBeTrue(); + _getResult.Errors[0].ShouldBeOfType(); + } + + private void ThenTheLoadBalancerIs() + { + _getResult.Data.ShouldBeOfType(); + } + + private void ThenItIsAdded() + { + _addResult.IsError.ShouldBe(false); + _addResult.ShouldBeOfType(); + } + + private void WhenIAddTheLoadBalancer() + { + _addResult = _loadBalancerHouse.Add(_key, _loadBalancer); + } + + + private void GivenThereIsALoadBalancer(string key, ILoadBalancer loadBalancer) + { + _key = key; + _loadBalancer = loadBalancer; + WhenIAddTheLoadBalancer(); + } + + private void WhenWeGetTheLoadBalancer(string key) + { + _getResult = _loadBalancerHouse.Get(key); + } + + private void ThenItIsReturned() + { + _getResult.Data.ShouldBe(_loadBalancer); + } + + class FakeLoadBalancer : ILoadBalancer + { + public Task> Lease() + { + throw new NotImplementedException(); + } + + public void Release(HostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } + + class FakeRoundRobinLoadBalancer : ILoadBalancer + { + public Task> Lease() + { + throw new NotImplementedException(); + } + + public void Release(HostAndPort hostAndPort) + { + throw new NotImplementedException(); + } + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs new file mode 100644 index 00000000..e76f3c2f --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -0,0 +1,212 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.Errors; +using Ocelot.Infrastructure.RequestData; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.LoadBalancer.Middleware; +using Ocelot.Logging; +using Ocelot.Responses; +using Ocelot.Values; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class LoadBalancerMiddlewareTests + { + private readonly Mock _loadBalancerHouse; + private readonly Mock _scopedRepository; + private readonly Mock _loadBalancer; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private HttpResponseMessage _result; + private HostAndPort _hostAndPort; + private OkResponse _downstreamUrl; + private OkResponse _downstreamRoute; + private ErrorResponse _getLoadBalancerHouseError; + private ErrorResponse _getHostAndPortError; + + public LoadBalancerMiddlewareTests() + { + _url = "http://localhost:51879"; + _loadBalancerHouse = new Mock(); + _scopedRepository = new Mock(); + _loadBalancer = new Mock(); + _loadBalancerHouse = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_loadBalancerHouse.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseLoadBalancingMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_scoped_data_repository_correctly() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithUpstreamHttpMethod("Get") + .Build()); + + this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheLoadBalancerHouseReturns()) + .And(x => x.GivenTheLoadBalancerReturns()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_set_pipeline_error_if_cannot_get_load_balancer() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithUpstreamHttpMethod("Get") + .Build()); + + this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheLoadBalancerHouseReturnsAnError()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenAnErrorStatingLoadBalancerCouldNotBeFoundIsSetOnPipeline()) + .BDDfy(); + } + + [Fact] + public void should_set_pipeline_error_if_cannot_get_least() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithUpstreamHttpMethod("Get") + .Build()); + + this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheLoadBalancerHouseReturns()) + .And(x => x.GivenTheLoadBalancerReturnsAnError()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenAnErrorStatingHostAndPortCouldNotBeFoundIsSetOnPipeline()) + .BDDfy(); + } + + private void GivenTheLoadBalancerReturnsAnError() + { + _getHostAndPortError = new ErrorResponse(new List() { new ServicesAreNullError($"services were null for bah") }); + _loadBalancer + .Setup(x => x.Lease()) + .ReturnsAsync(_getHostAndPortError); + } + + private void GivenTheLoadBalancerReturns() + { + _hostAndPort = new HostAndPort("127.0.0.1", 80); + _loadBalancer + .Setup(x => x.Lease()) + .ReturnsAsync(new OkResponse(_hostAndPort)); + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + private void GivenTheLoadBalancerHouseReturns() + { + _loadBalancerHouse + .Setup(x => x.Get(It.IsAny())) + .Returns(new OkResponse(_loadBalancer.Object)); + } + + + private void GivenTheLoadBalancerHouseReturnsAnError() + { + _getLoadBalancerHouseError = new ErrorResponse(new List() + { + new UnableToFindLoadBalancerError($"unabe to find load balancer for bah") + }); + + _loadBalancerHouse + .Setup(x => x.Get(It.IsAny())) + .Returns(_getLoadBalancerHouseError); + } + + private void ThenTheScopedDataRepositoryIsCalledCorrectly() + { + _scopedRepository + .Verify(x => x.Add("HostAndPort", _hostAndPort), Times.Once()); + } + + private void ThenAnErrorStatingLoadBalancerCouldNotBeFoundIsSetOnPipeline() + { + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareError", true), Times.Once); + + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareErrors", _getLoadBalancerHouseError.Errors), Times.Once); + } + + private void ThenAnErrorSayingReleaseFailedIsSetOnThePipeline() + { + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareError", true), Times.Once); + + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareErrors", It.IsAny>()), Times.Once); + } + + private void ThenAnErrorStatingHostAndPortCouldNotBeFoundIsSetOnPipeline() + { + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareError", true), Times.Once); + + _scopedRepository + .Verify(x => x.Add("OcelotMiddlewareErrors", _getHostAndPortError.Errors), Times.Once); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheDownStreamUrlIs(string downstreamUrl) + { + _downstreamUrl = new OkResponse(downstreamUrl); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamUrl); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs new file mode 100644 index 00000000..ac89a6d0 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class NoLoadBalancerTests + { + private List _services; + private NoLoadBalancer _loadBalancer; + private Response _result; + + [Fact] + public void should_return_host_and_port() + { + var hostAndPort = new HostAndPort("127.0.0.1", 80); + + var services = new List + { + new Service("product", hostAndPort, string.Empty, string.Empty, new string[0]) + }; + this.Given(x => x.GivenServices(services)) + .When(x => x.WhenIGetTheNextHostAndPort()) + .Then(x => x.ThenTheHostAndPortIs(hostAndPort)) + .BDDfy(); + } + + private void GivenServices(List services) + { + _services = services; + } + + private void WhenIGetTheNextHostAndPort() + { + _loadBalancer = new NoLoadBalancer(_services); + _result = _loadBalancer.Lease().Result; + } + + private void ThenTheHostAndPortIs(HostAndPort expected) + { + _result.Data.ShouldBe(expected); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs new file mode 100644 index 00000000..f2ef5367 --- /dev/null +++ b/test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using System.Diagnostics; +using Ocelot.LoadBalancer.LoadBalancers; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.LoadBalancer +{ + public class RoundRobinTests + { + private readonly RoundRobinLoadBalancer _roundRobin; + private readonly List _services; + private Response _hostAndPort; + + public RoundRobinTests() + { + _services = new List + { + new Service("product", new HostAndPort("127.0.0.1", 5000), string.Empty, string.Empty, new string[0]), + new Service("product", new HostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, new string[0]), + new Service("product", new HostAndPort("127.0.0.1", 5001), string.Empty, string.Empty, new string[0]) + }; + + _roundRobin = new RoundRobinLoadBalancer(_services); + } + + [Fact] + public void should_get_next_address() + { + this.Given(x => x.GivenIGetTheNextAddress()) + .Then(x => x.ThenTheNextAddressIndexIs(0)) + .Given(x => x.GivenIGetTheNextAddress()) + .Then(x => x.ThenTheNextAddressIndexIs(1)) + .Given(x => x.GivenIGetTheNextAddress()) + .Then(x => x.ThenTheNextAddressIndexIs(2)) + .BDDfy(); + } + + [Fact] + public void should_go_back_to_first_address_after_finished_last() + { + var stopWatch = Stopwatch.StartNew(); + + while (stopWatch.ElapsedMilliseconds < 1000) + { + var address = _roundRobin.Lease().Result; + address.Data.ShouldBe(_services[0].HostAndPort); + address = _roundRobin.Lease().Result; + address.Data.ShouldBe(_services[1].HostAndPort); + address = _roundRobin.Lease().Result; + address.Data.ShouldBe(_services[2].HostAndPort); + } + } + + private void GivenIGetTheNextAddress() + { + _hostAndPort = _roundRobin.Lease().Result; + } + + private void ThenTheNextAddressIndexIs(int index) + { + _hostAndPort.Data.ShouldBe(_services[index].HostAndPort); + } + } +} diff --git a/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs b/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs new file mode 100644 index 00000000..c0777a92 --- /dev/null +++ b/test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Moq; +using Ocelot.Middleware; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Middleware +{ + public class BaseUrlFinderTests + { + private readonly BaseUrlFinder _baseUrlFinder; + private readonly Mock _webHostBuilder; + private string _result; + + public BaseUrlFinderTests() + { + _webHostBuilder = new Mock(); + _baseUrlFinder = new BaseUrlFinder(_webHostBuilder.Object); + } + + [Fact] + public void should_find_base_url_based_on_webhostbuilder() + { + this.Given(x => GivenTheWebHostBuilderReturns("http://localhost:7000")) + .When(x => WhenIFindTheUrl()) + .Then(x => ThenTheUrlIs("http://localhost:7000")) + .BDDfy(); + } + + [Fact] + public void should_use_default_base_url() + { + this.Given(x => GivenTheWebHostBuilderReturns("")) + .When(x => WhenIFindTheUrl()) + .Then(x => ThenTheUrlIs("http://localhost:5000")) + .BDDfy(); + } + + private void GivenTheWebHostBuilderReturns(string url) + { + _webHostBuilder + .Setup(x => x.GetSetting(WebHostDefaults.ServerUrlsKey)) + .Returns(url); + } + + private void WhenIFindTheUrl() + { + _result = _baseUrlFinder.Find(); + } + + private void ThenTheUrlIs(string expected) + { + _result.ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs index e4c7375e..f381ff1b 100644 --- a/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs @@ -65,11 +65,12 @@ namespace Ocelot.UnitTests.QueryStrings { var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() - .WithDownstreamTemplate("any old string") + .WithDownstreamPathTemplate("any old string") .WithClaimsToQueries(new List { new ClaimToThing("UserId", "Subject", "", 0) }) + .WithUpstreamHttpMethod("Get") .Build()); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs new file mode 100644 index 00000000..8b9b40ee --- /dev/null +++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs @@ -0,0 +1,149 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Infrastructure.RequestData; +using Ocelot.RateLimit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Logging; +using System.IO; +using Ocelot.RateLimit.Middleware; +using Ocelot.DownstreamRouteFinder; +using Ocelot.Responses; +using Xunit; +using TestStack.BDDfy; +using Ocelot.Configuration.Builder; +using Shouldly; +using Ocelot.Configuration; + +namespace Ocelot.UnitTests.RateLimit +{ + public class ClientRateLimitMiddlewareTests + { + private readonly Mock _scopedRepository; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private OkResponse _downstreamRoute; + private int responseStatusCode; + + public ClientRateLimitMiddlewareTests() + { + _url = "http://localhost:51879/api/ClientRateLimit"; + _scopedRepository = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddMemoryCache(); + x.AddSingleton(); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseRateLimiting(); + app.Run(async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync("This is ratelimit test"); + }); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + + [Fact] + public void should_call_middleware_and_ratelimiting() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions( + new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List(), false, "", "", new Ocelot.Configuration.RateLimitRule("1s", TimeSpan.FromSeconds(100), 3), 429)) + .WithUpstreamHttpMethod("Get") + .Build()); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .When(x => x.WhenICallTheMiddlewareMultipleTime(2)) + .Then(x => x.ThenresponseStatusCodeIs200()) + .When(x => x.WhenICallTheMiddlewareMultipleTime(2)) + .Then(x => x.ThenresponseStatusCodeIs429()) + .BDDfy(); + } + + [Fact] + public void should_call_middleware_withWhitelistClient() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions( + new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List() { "ocelotclient2" }, false, "", "", new RateLimitRule( "1s", TimeSpan.FromSeconds(100),3),429)) + .WithUpstreamHttpMethod("Get") + .Build()); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .When(x => x.WhenICallTheMiddlewareWithWhiteClient()) + .Then(x => x.ThenresponseStatusCodeIs200()) + .BDDfy(); + } + + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + private void WhenICallTheMiddlewareMultipleTime(int times) + { + var clientId = "ocelotclient1"; + // Act + for (int i = 0; i < times; i++) + { + var request = new HttpRequestMessage(new HttpMethod("GET"), _url); + request.Headers.Add("ClientId", clientId); + + var response = _client.SendAsync(request); + responseStatusCode = (int)response.Result.StatusCode; + } + + } + + private void WhenICallTheMiddlewareWithWhiteClient() + { + var clientId = "ocelotclient2"; + // Act + for (int i = 0; i < 10; i++) + { + var request = new HttpRequestMessage(new HttpMethod("GET"), _url); + request.Headers.Add("ClientId", clientId); + + var response = _client.SendAsync(request); + responseStatusCode = (int)response.Result.StatusCode; + } + } + + private void ThenresponseStatusCodeIs429() + { + responseStatusCode.ShouldBe(429); + } + + private void ThenresponseStatusCodeIs200() + { + responseStatusCode.ShouldBe(200); + } + } +} diff --git a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs index e2f81abe..b674b015 100644 --- a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs @@ -19,6 +19,8 @@ using Ocelot.Request.Middleware; using Ocelot.Responses; using TestStack.BDDfy; using Xunit; +using Ocelot.Configuration; +using Ocelot.Requester.QoS; namespace Ocelot.UnitTests.Request { @@ -26,6 +28,7 @@ namespace Ocelot.UnitTests.Request { private readonly Mock _requestBuilder; private readonly Mock _scopedRepository; + private readonly Mock _qosProviderHouse; private readonly string _url; private readonly TestServer _server; private readonly HttpClient _client; @@ -37,6 +40,7 @@ namespace Ocelot.UnitTests.Request public HttpRequestBuilderMiddlewareTests() { _url = "http://localhost:51879"; + _qosProviderHouse = new Mock(); _requestBuilder = new Mock(); _scopedRepository = new Mock(); var builder = new WebHostBuilder() @@ -44,6 +48,7 @@ namespace Ocelot.UnitTests.Request { x.AddSingleton(); x.AddLogging(); + x.AddSingleton(_qosProviderHouse.Object); x.AddSingleton(_requestBuilder.Object); x.AddSingleton(_scopedRepository.Object); }) @@ -67,17 +72,26 @@ namespace Ocelot.UnitTests.Request var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() - .WithRequestIdKey("LSRequestId").Build()); - + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod("Get") + .Build()); this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) + .And(x => x.GivenTheQosProviderHouseReturns(new OkResponse(new NoQoSProvider()))) .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) - .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), new CookieContainer()))) + .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), new CookieContainer(), true, new NoQoSProvider()))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) .BDDfy(); } + private void GivenTheQosProviderHouseReturns(Response qosProvider) + { + _qosProviderHouse + .Setup(x => x.Get(It.IsAny())) + .Returns(qosProvider); + } + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) { _downstreamRoute = new OkResponse(downstreamRoute); @@ -91,7 +105,7 @@ namespace Ocelot.UnitTests.Request _request = new OkResponse(request); _requestBuilder .Setup(x => x.Build(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), - It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(),It.IsAny(), It.IsAny())) .ReturnsAsync(_request); } diff --git a/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs b/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs index 3a07532e..9d1c2ed5 100644 --- a/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs +++ b/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs @@ -10,6 +10,8 @@ using Ocelot.Responses; using Shouldly; using TestStack.BDDfy; using Xunit; +using Ocelot.Configuration; +using Ocelot.Requester.QoS; namespace Ocelot.UnitTests.Request { @@ -25,6 +27,8 @@ namespace Ocelot.UnitTests.Request private readonly IRequestCreator _requestCreator; private Response _result; private Ocelot.RequestId.RequestId _requestId; + private bool _isQos; + private IQoSProvider _qoSProvider; public RequestBuilderTests() { @@ -37,6 +41,7 @@ namespace Ocelot.UnitTests.Request { this.Given(x => x.GivenIHaveHttpMethod("GET")) .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x=> x.GivenTheQos(true, new NoQoSProvider())) .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectDownstreamUrlIsUsed("http://www.bbc.co.uk/")) .BDDfy(); @@ -47,6 +52,8 @@ namespace Ocelot.UnitTests.Request { this.Given(x => x.GivenIHaveHttpMethod("POST")) .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) + .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectHttpMethodIsUsed(HttpMethod.Post)) .BDDfy(); @@ -59,7 +66,9 @@ namespace Ocelot.UnitTests.Request .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) .And(x => x.GivenIHaveTheHttpContent(new StringContent("Hi from Tom"))) .And(x => x.GivenTheContentTypeIs("application/json")) - .When(x => x.WhenICreateARequest()) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) + + .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectContentIsUsed(new StringContent("Hi from Tom"))) .BDDfy(); } @@ -71,6 +80,8 @@ namespace Ocelot.UnitTests.Request .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) .And(x => x.GivenIHaveTheHttpContent(new StringContent("Hi from Tom"))) .And(x => x.GivenTheContentTypeIs("application/json")) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) + .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectContentHeadersAreUsed(new HeaderDictionary { @@ -88,6 +99,8 @@ namespace Ocelot.UnitTests.Request .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) .And(x => x.GivenIHaveTheHttpContent(new StringContent("Hi from Tom"))) .And(x => x.GivenTheContentTypeIs("application/json; charset=utf-8")) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) + .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectContentHeadersAreUsed(new HeaderDictionary { @@ -107,6 +120,8 @@ namespace Ocelot.UnitTests.Request { {"ChopSticks", "Bubbles" } })) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) + .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectHeadersAreUsed(new HeaderDictionary { @@ -124,7 +139,8 @@ namespace Ocelot.UnitTests.Request .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) .And(x => x.GivenTheHttpHeadersAre(new HeaderDictionary())) .And(x => x.GivenTheRequestIdIs(new Ocelot.RequestId.RequestId("RequestId", requestId))) - .When(x => x.WhenICreateARequest()) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) + .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectHeadersAreUsed(new HeaderDictionary { {"RequestId", requestId } @@ -142,7 +158,8 @@ namespace Ocelot.UnitTests.Request {"RequestId", "534534gv54gv45g" } })) .And(x => x.GivenTheRequestIdIs(new Ocelot.RequestId.RequestId("RequestId", Guid.NewGuid().ToString()))) - .When(x => x.WhenICreateARequest()) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) + .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheCorrectHeadersAreUsed(new HeaderDictionary { {"RequestId", "534534gv54gv45g" } @@ -161,7 +178,8 @@ namespace Ocelot.UnitTests.Request .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) .And(x => x.GivenTheHttpHeadersAre(new HeaderDictionary())) .And(x => x.GivenTheRequestIdIs(new Ocelot.RequestId.RequestId(requestIdKey, requestIdValue))) - .When(x => x.WhenICreateARequest()) + .And(x => x.GivenTheQos(true, new NoQoSProvider())) + .When(x => x.WhenICreateARequest()) .And(x => x.ThenTheRequestIdIsNotInTheHeaders()) .BDDfy(); } @@ -171,6 +189,12 @@ namespace Ocelot.UnitTests.Request _requestId = requestId; } + private void GivenTheQos(bool isQos, IQoSProvider qoSProvider) + { + _isQos = isQos; + _qoSProvider = qoSProvider; + } + [Fact] public void should_use_cookies() { @@ -281,7 +305,7 @@ namespace Ocelot.UnitTests.Request private void WhenICreateARequest() { _result = _requestCreator.Build(_httpMethod, _downstreamUrl, _content?.ReadAsStreamAsync().Result, _headers, - _cookies, _query, _contentType, _requestId).Result; + _cookies, _query, _contentType, _requestId,_isQos,_qoSProvider).Result; } diff --git a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs index 8c023783..26450ab8 100644 --- a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs @@ -71,8 +71,10 @@ namespace Ocelot.UnitTests.RequestId { var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() - .WithDownstreamTemplate("any old string") - .WithRequestIdKey("LSRequestId").Build()); + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod("Get") + .Build()); var requestId = Guid.NewGuid().ToString(); @@ -88,8 +90,10 @@ namespace Ocelot.UnitTests.RequestId { var downstreamRoute = new DownstreamRoute(new List(), new ReRouteBuilder() - .WithDownstreamTemplate("any old string") - .WithRequestIdKey("LSRequestId").Build()); + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod("Get") + .Build()); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) .When(x => x.WhenICallTheMiddleware()) diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index d99a99eb..37815a64 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -13,6 +13,7 @@ using Ocelot.Logging; using Ocelot.QueryStrings.Middleware; using Ocelot.Requester; using Ocelot.Requester.Middleware; +using Ocelot.Requester.QoS; using Ocelot.Responder; using Ocelot.Responses; using TestStack.BDDfy; @@ -61,7 +62,7 @@ namespace Ocelot.UnitTests.Requester [Fact] public void should_call_scoped_data_repository_correctly() { - this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage(),new CookieContainer()))) + this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage(),new CookieContainer(),true, new NoQoSProvider()))) .And(x => x.GivenTheRequesterReturns(new HttpResponseMessage())) .And(x => x.GivenTheScopedRepoReturns()) .When(x => x.WhenICallTheMiddleware()) diff --git a/test/Ocelot.UnitTests/Requester/QoSProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/QoSProviderFactoryTests.cs new file mode 100644 index 00000000..f4beb0f1 --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/QoSProviderFactoryTests.cs @@ -0,0 +1,80 @@ +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Requester.QoS; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class QoSProviderFactoryTests + { + private readonly IQoSProviderFactory _factory; + private ReRoute _reRoute; + private IQoSProvider _result; + private Mock _loggerFactory; + private Mock _logger; + + public QoSProviderFactoryTests() + { + _logger = new Mock(); + _loggerFactory = new Mock(); + _loggerFactory + .Setup(x => x.CreateLogger()) + .Returns(_logger.Object); + _factory = new QoSProviderFactory(_loggerFactory.Object); + } + + [Fact] + public void should_return_no_qos_provider() + { + var reRoute = new ReRouteBuilder() + .WithUpstreamHttpMethod("get") + .WithIsQos(false) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .When(x => x.WhenIGetTheQoSProvider()) + .Then(x => x.ThenTheQoSProviderIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_return_polly_qos_provider() + { + var qosOptions = new QoSOptionsBuilder() + .WithTimeoutValue(100) + .WithDurationOfBreak(100) + .WithExceptionsAllowedBeforeBreaking(100) + .Build(); + + var reRoute = new ReRouteBuilder() + .WithUpstreamHttpMethod("get") + .WithIsQos(true) + .WithQosOptions(qosOptions) + .Build(); + + this.Given(x => x.GivenAReRoute(reRoute)) + .When(x => x.WhenIGetTheQoSProvider()) + .Then(x => x.ThenTheQoSProviderIsReturned()) + .BDDfy(); + } + + private void GivenAReRoute(ReRoute reRoute) + { + _reRoute = reRoute; + } + + private void WhenIGetTheQoSProvider() + { + _result = _factory.Get(_reRoute); + } + + private void ThenTheQoSProviderIsReturned() + { + _result.ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs b/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs new file mode 100644 index 00000000..1e76a027 --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/QosProviderHouseTests.cs @@ -0,0 +1,117 @@ +using Ocelot.Requester.QoS; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class QosProviderHouseTests + { + private IQoSProvider _qoSProvider; + private readonly QosProviderHouse _qosProviderHouse; + private Response _addResult; + private Response _getResult; + private string _key; + + public QosProviderHouseTests() + { + _qosProviderHouse = new QosProviderHouse(); + } + + [Fact] + public void should_store_qos_provider() + { + var key = "test"; + + this.Given(x => x.GivenThereIsAQoSProvider(key, new FakeQoSProvider())) + .When(x => x.WhenIAddTheQoSProvider()) + .Then(x => x.ThenItIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_get_qos_provider() + { + var key = "test"; + + this.Given(x => x.GivenThereIsAQoSProvider(key, new FakeQoSProvider())) + .When(x => x.WhenWeGetTheQoSProvider(key)) + .Then(x => x.ThenItIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_store_qos_providers_by_key() + { + var key = "test"; + var keyTwo = "testTwo"; + + this.Given(x => x.GivenThereIsAQoSProvider(key, new FakeQoSProvider())) + .And(x => x.GivenThereIsAQoSProvider(keyTwo, new FakePollyQoSProvider())) + .When(x => x.WhenWeGetTheQoSProvider(key)) + .Then(x => x.ThenTheQoSProviderIs()) + .When(x => x.WhenWeGetTheQoSProvider(keyTwo)) + .Then(x => x.ThenTheQoSProviderIs()) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_no_qos_provider_with_key() + { + this.When(x => x.WhenWeGetTheQoSProvider("test")) + .Then(x => x.ThenAnErrorIsReturned()) + .BDDfy(); + } + + private void ThenAnErrorIsReturned() + { + _getResult.IsError.ShouldBeTrue(); + _getResult.Errors[0].ShouldBeOfType(); + } + + private void ThenTheQoSProviderIs() + { + _getResult.Data.ShouldBeOfType(); + } + + private void ThenItIsAdded() + { + _addResult.IsError.ShouldBe(false); + _addResult.ShouldBeOfType(); + } + + private void WhenIAddTheQoSProvider() + { + _addResult = _qosProviderHouse.Add(_key, _qoSProvider); + } + + + private void GivenThereIsAQoSProvider(string key, IQoSProvider qoSProvider) + { + _key = key; + _qoSProvider = qoSProvider; + WhenIAddTheQoSProvider(); + } + + private void WhenWeGetTheQoSProvider(string key) + { + _getResult = _qosProviderHouse.Get(key); + } + + private void ThenItIsReturned() + { + _getResult.Data.ShouldBe(_qoSProvider); + } + + class FakeQoSProvider : IQoSProvider + { + public CircuitBreaker CircuitBreaker { get; } + } + + class FakePollyQoSProvider : IQoSProvider + { + public CircuitBreaker CircuitBreaker { get; } + } + } +} diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index 78f78235..d2ac91e0 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; -using System.IO; -using System.Net.Http; -using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; using Ocelot.Errors; using Ocelot.Middleware; +using Ocelot.Requester; using Ocelot.Responder; using Ocelot.Responses; using Shouldly; @@ -23,6 +22,18 @@ namespace Ocelot.UnitTests.Responder _codeMapper = new ErrorsToHttpStatusCodeMapper(); } + [Fact] + public void should_return_timeout() + { + this.Given(x => x.GivenThereAreErrors(new List + { + new RequestTimedOutError(new Exception()) + })) + .When(x => x.WhenIGetErrorStatusCode()) + .Then(x => x.ThenTheResponseIsStatusCodeIs(503)) + .BDDfy(); + } + [Fact] public void should_create_unauthenticated_response_code() { diff --git a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs index b643028e..09a5c22c 100644 --- a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs @@ -62,19 +62,11 @@ namespace Ocelot.UnitTests.Responder { this.Given(x => x.GivenTheHttpResponseMessageIs(new HttpResponseMessage())) .And(x => x.GivenThereAreNoPipelineErrors()) - .And(x => x.GivenTheResponderReturns()) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenThereAreNoErrors()) .BDDfy(); } - private void GivenTheResponderReturns() - { - _responder - .Setup(x => x.SetResponseOnHttpContext(It.IsAny(), It.IsAny())) - .ReturnsAsync(new OkResponse()); - } - private void GivenThereAreNoPipelineErrors() { _scopedRepository diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs new file mode 100644 index 00000000..282f8ec1 --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Ocelot.ServiceDiscovery; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ + public class ConfigurationServiceProviderTests + { + private ConfigurationServiceProvider _serviceProvider; + private List _result; + private List _expected; + + [Fact] + public void should_return_services() + { + var hostAndPort = new HostAndPort("127.0.0.1", 80); + + var services = new List + { + new Service("product", hostAndPort, string.Empty, string.Empty, new string[0]) + }; + + this.Given(x => x.GivenServices(services)) + .When(x => x.WhenIGetTheService()) + .Then(x => x.ThenTheFollowingIsReturned(services)) + .BDDfy(); + } + + private void GivenServices(List services) + { + _expected = services; + } + + private void WhenIGetTheService() + { + _serviceProvider = new ConfigurationServiceProvider(_expected); + _result = _serviceProvider.Get().Result; + } + + private void ThenTheFollowingIsReturned(List services) + { + _result[0].HostAndPort.DownstreamHost.ShouldBe(services[0].HostAndPort.DownstreamHost); + + _result[0].HostAndPort.DownstreamPort.ShouldBe(services[0].HostAndPort.DownstreamPort); + + _result[0].Name.ShouldBe(services[0].Name); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs new file mode 100644 index 00000000..b3053afa --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs @@ -0,0 +1,66 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.ServiceDiscovery; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ + public class ServiceProviderFactoryTests + { + private ServiceProviderConfiguraion _serviceConfig; + private IServiceDiscoveryProvider _result; + private readonly ServiceDiscoveryProviderFactory _factory; + + public ServiceProviderFactoryTests() + { + _factory = new ServiceDiscoveryProviderFactory(); + } + + [Fact] + public void should_return_no_service_provider() + { + var serviceConfig = new ServiceProviderConfiguraionBuilder() + .WithDownstreamHost("127.0.0.1") + .WithDownstreamPort(80) + .WithUseServiceDiscovery(false) + .Build(); + + this.Given(x => x.GivenTheReRoute(serviceConfig)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + [Fact] + public void should_return_consul_service_provider() + { + var serviceConfig = new ServiceProviderConfiguraionBuilder() + .WithServiceName("product") + .WithUseServiceDiscovery(true) + .WithServiceDiscoveryProvider("Consul") + .Build(); + + this.Given(x => x.GivenTheReRoute(serviceConfig)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + + private void GivenTheReRoute(ServiceProviderConfiguraion serviceConfig) + { + _serviceConfig = serviceConfig; + } + + private void WhenIGetTheServiceProvider() + { + _result = _factory.Get(_serviceConfig); + } + + private void ThenTheServiceProviderIs() + { + _result.ShouldBeOfType(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs new file mode 100644 index 00000000..87425329 --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.ServiceDiscovery +{ + public class ServiceRegistryTests + { + private Service _service; + private List _services; + private ServiceRegistry _serviceRegistry; + private ServiceRepository _serviceRepository; + + public ServiceRegistryTests() + { + _serviceRepository = new ServiceRepository(); + _serviceRegistry = new ServiceRegistry(_serviceRepository); + } + + [Fact] + public void should_register_service() + { + this.Given(x => x.GivenAServiceToRegister("product", "localhost:5000", 80)) + .When(x => x.WhenIRegisterTheService()) + .Then(x => x.ThenTheServiceIsRegistered()) + .BDDfy(); + } + + public void should_lookup_service() + { + this.Given(x => x.GivenAServiceIsRegistered("product", "localhost:600", 80)) + .When(x => x.WhenILookupTheService("product")) + .Then(x => x.ThenTheServiceDetailsAreReturned()) + .BDDfy(); + } + + private void ThenTheServiceDetailsAreReturned() + { + _services[0].HostAndPort.DownstreamHost.ShouldBe(_service.HostAndPort.DownstreamHost); + _services[0].HostAndPort.DownstreamPort.ShouldBe(_service.HostAndPort.DownstreamPort); + _services[0].Name.ShouldBe(_service.Name); + } + + private void WhenILookupTheService(string name) + { + _services = _serviceRegistry.Lookup(name); + } + + private void GivenAServiceIsRegistered(string name, string address, int port) + { + _service = new Service(name, new HostAndPort(address, port), string.Empty, string.Empty, new string[0]); + _serviceRepository.Set(_service); + } + + private void GivenAServiceToRegister(string name, string address, int port) + { + _service = new Service(name, new HostAndPort(address, port), string.Empty, string.Empty, new string[0]); + } + + private void WhenIRegisterTheService() + { + _serviceRegistry.Register(_service); + } + + private void ThenTheServiceIsRegistered() + { + var serviceNameAndAddress = _serviceRepository.Get(_service.Name); + serviceNameAndAddress[0].HostAndPort.DownstreamHost.ShouldBe(_service.HostAndPort.DownstreamHost); + serviceNameAndAddress[0].HostAndPort.DownstreamPort.ShouldBe(_service.HostAndPort.DownstreamPort); + serviceNameAndAddress[0].Name.ShouldBe(_service.Name); + } + } + + public interface IServiceRegistry + { + void Register(Service serviceNameAndAddress); + List Lookup(string name); + } + + public class ServiceRegistry : IServiceRegistry + { + private readonly IServiceRepository _repository; + public ServiceRegistry(IServiceRepository repository) + { + _repository = repository; + } + + public void Register(Service serviceNameAndAddress) + { + _repository.Set(serviceNameAndAddress); + } + + public List Lookup(string name) + { + return _repository.Get(name); + } + } + + public interface IServiceRepository + { + List Get(string serviceName); + void Set(Service serviceNameAndAddress); + } + + public class ServiceRepository : IServiceRepository + { + private Dictionary> _registeredServices; + + public ServiceRepository() + { + _registeredServices = new Dictionary>(); + } + + public List Get(string serviceName) + { + return _registeredServices[serviceName]; + } + + public void Set(Service serviceNameAndAddress) + { + List services; + if(_registeredServices.TryGetValue(serviceNameAndAddress.Name, out services)) + { + services.Add(serviceNameAndAddress); + _registeredServices[serviceNameAndAddress.Name] = services; + } + else + { + _registeredServices[serviceNameAndAddress.Name] = new List(){ serviceNameAndAddress }; + } + + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/configuration.json b/test/Ocelot.UnitTests/configuration.json new file mode 100755 index 00000000..61548dab --- /dev/null +++ b/test/Ocelot.UnitTests/configuration.json @@ -0,0 +1 @@ +{"ReRoutes":[{"DownstreamPathTemplate":"/test/test/{test}","UpstreamPathTemplate":null,"UpstreamHttpMethod":null,"AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"https","DownstreamHost":"localhost","DownstreamPort":80,"QoSOptions":{"ExceptionsAllowedBeforeBreaking":0,"DurationOfBreak":0,"TimeoutValue":0},"LoadBalancer":null}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":"consul","Host":"blah","Port":198},"AdministrationPath":"testy"}} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/project.json b/test/Ocelot.UnitTests/project.json index beb19321..e0b93748 100644 --- a/test/Ocelot.UnitTests/project.json +++ b/test/Ocelot.UnitTests/project.json @@ -1,37 +1,42 @@ { - "version": "1.0.0-*", + "version": "0.0.0-dev", - "testRunner": "xunit", + "testRunner": "xunit", "dependencies": { - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", - "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0", - "Microsoft.Extensions.Configuration.Json": "1.0.0", - "Microsoft.Extensions.Logging": "1.0.0", - "Microsoft.Extensions.Logging.Console": "1.0.0", - "Microsoft.Extensions.Logging.Debug": "1.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0", - "Microsoft.AspNetCore.Http": "1.0.0", - "Ocelot": "1.0.0-*", - "xunit": "2.2.0-beta2-build3300", "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.AspNetCore.TestHost": "1.1.0", + "Microsoft.DotNet.InternalAbstractions": "1.0.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", "Moq": "4.6.38-alpha", - "Microsoft.AspNetCore.TestHost": "1.0.0", - "Microsoft.AspNetCore.Mvc": "1.0.1", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", - "Microsoft.NETCore.App": { - "version": "1.0.1", - "type": "platform" - }, + "Ocelot": "0.0.0-dev", "Shouldly": "2.8.2", - "TestStack.BDDfy": "4.3.2" + "TestStack.BDDfy": "4.3.2", + "xunit": "2.2.0-rc1-build3507" }, - - "frameworks": { - "netcoreapp1.4": { - "imports": [ - ] - } + "runtimes": { + "osx.10.11-x64":{}, + "osx.10.12-x64":{}, + "win7-x64": {}, + "win10-x64": {} + }, + "frameworks": { + "netcoreapp1.1": { + "imports": [ + ] } + } } diff --git a/test/Ocelot.UnitTests/project.json.orig b/test/Ocelot.UnitTests/project.json.orig new file mode 100644 index 00000000..5feffd8d --- /dev/null +++ b/test/Ocelot.UnitTests/project.json.orig @@ -0,0 +1,50 @@ +{ + "version": "0.0.0-dev", + + "testRunner": "xunit", + + "dependencies": { + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "Ocelot": "0.0.0-dev", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Moq": "4.6.38-alpha", + "Microsoft.AspNetCore.TestHost": "1.1.0", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "Shouldly": "2.8.2", + "TestStack.BDDfy": "4.3.2", + "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", + "Microsoft.DotNet.InternalAbstractions": "1.0.0", +<<<<<<< HEAD + "Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0" +======= + "xunit": "2.2.0-rc1-build3507" +>>>>>>> develop + }, + "runtimes": { + "osx.10.11-x64":{}, +<<<<<<< HEAD + "osx.10.12-x64": {}, + "win7-x64": {} +======= + "osx.10.12-x64":{}, + "win7-x64": {}, + "win10-x64": {} +>>>>>>> develop + }, + "frameworks": { + "netcoreapp1.1": { + "imports": [ + ] + } + } +} diff --git a/version.ps1 b/version.ps1 new file mode 100644 index 00000000..621201b6 --- /dev/null +++ b/version.ps1 @@ -0,0 +1 @@ +.\tools\GitVersion.CommandLine\tools\GitVersion.exe \ No newline at end of file