mirror of
https://github.com/nsnail/Ocelot.git
synced 2025-04-23 00:32:50 +08:00
merged develop and stolen binarymash dont publish unstable build script code
This commit is contained in:
commit
d33e0c6f3b
125
README.md
125
README.md
@ -32,9 +32,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
|
||||
@ -63,11 +60,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.
|
||||
|
||||
{
|
||||
```json
|
||||
{
|
||||
"ReRoutes": [],
|
||||
"GlobalConfiguration": {}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
More information on how to use these options is below..
|
||||
|
||||
## Startup
|
||||
@ -75,8 +73,9 @@ 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)
|
||||
{
|
||||
var builder = new ConfigurationBuilder()
|
||||
@ -113,23 +112,27 @@ Currently this is the only way to get configuration into Ocelot.
|
||||
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();
|
||||
```csharp
|
||||
IWebHostBuilder builder = new WebHostBuilder();
|
||||
|
||||
builder.ConfigureServices(s => {
|
||||
builder.ConfigureServices(s => {
|
||||
s.AddSingleton(builder);
|
||||
});
|
||||
});
|
||||
|
||||
builder.UseKestrel()
|
||||
builder.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseStartup<Startup>();
|
||||
|
||||
var host = builder.Build();
|
||||
var host = builder.Build();
|
||||
|
||||
host.Run();
|
||||
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.
|
||||
@ -147,22 +150,26 @@ 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
|
||||
@ -176,7 +183,9 @@ 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
|
||||
@ -194,10 +203,11 @@ a url you would like to route through with Ocelot as this will not work. The adm
|
||||
MapWhen functionality of asp.net core and all requests to root/administration will be sent there not
|
||||
to the Ocelot middleware.
|
||||
|
||||
"GlobalConfiguration": {
|
||||
```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!
|
||||
|
||||
@ -235,25 +245,28 @@ At the moment the only supported service discovery provider is Consul. The follo
|
||||
GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default
|
||||
will be used.
|
||||
|
||||
"ServiceDiscoveryProvider":
|
||||
{
|
||||
```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"
|
||||
"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.
|
||||
@ -265,7 +278,8 @@ Ocelot currently supports the use of bearer tokens with Identity Server (more pr
|
||||
come if required). In order to identity a ReRoute as authenticated it needs the following
|
||||
configuration added.
|
||||
|
||||
"AuthenticationOptions": {
|
||||
```json
|
||||
"AuthenticationOptions": {
|
||||
"Provider": "IdentityServer",
|
||||
"ProviderRootUrl": "http://localhost:52888",
|
||||
"ScopeName": "api",
|
||||
@ -274,7 +288,8 @@ configuration added.
|
||||
"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
|
||||
@ -293,9 +308,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": {
|
||||
```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.
|
||||
@ -334,10 +351,12 @@ and add whatever was at the index requested to the transform.
|
||||
|
||||
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".
|
||||
@ -346,9 +365,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": {
|
||||
```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".
|
||||
@ -357,9 +378,11 @@ CustomerId header. Assuming the sub looks like this "usertypevalue|useridvalue".
|
||||
|
||||
Below is an example configuration that will transforms claims to query string parameters
|
||||
|
||||
"AddQueriesToRequest": {
|
||||
```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.
|
||||
@ -372,11 +395,13 @@ want to use a circuit breaker when making requests to a downstream service. This
|
||||
|
||||
Add the following section to a ReRoute configuration.
|
||||
|
||||
"QoSOptions": {
|
||||
```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.
|
||||
@ -397,14 +422,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.
|
||||
|
||||
@ -423,7 +452,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.
|
||||
|
||||
@ -435,15 +466,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
|
||||
{
|
||||
```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.
|
||||
|
564
README.md.orig
Normal file
564
README.md.orig
Normal file
@ -0,0 +1,564 @@
|
||||
# Ocelot
|
||||
|
||||
[](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb)
|
||||
|
||||
[](https://gitter.im/Ocelotey/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
[ 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<ConfigurationBuilderCachePart> 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<Startup>();
|
||||
|
||||
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<T> 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)
|
||||
|
||||
|
||||
|
65
build.cake
65
build.cake
@ -46,9 +46,12 @@ var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package";
|
||||
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);
|
||||
|
||||
@ -76,13 +79,15 @@ Task("Clean")
|
||||
Task("Version")
|
||||
.Does(() =>
|
||||
{
|
||||
var nugetVersion = GetNuGetVersionForCommit();
|
||||
versioning = GetNuGetVersionForCommit();
|
||||
var nugetVersion = versioning.NuGetVersion;
|
||||
|
||||
Information("SemVer version number: " + nugetVersion);
|
||||
|
||||
if (AppVeyor.IsRunningOnAppVeyor)
|
||||
{
|
||||
Information("Persisting version number...");
|
||||
PersistVersion(nugetVersion);
|
||||
PersistVersion(committedVersion, nugetVersion);
|
||||
buildVersion = nugetVersion;
|
||||
}
|
||||
else
|
||||
@ -179,7 +184,7 @@ Task("CreatePackages")
|
||||
{
|
||||
EnsureDirectoryExists(packagesDir);
|
||||
|
||||
GenerateReleaseNotes();
|
||||
GenerateReleaseNotes(releaseNotesFile);
|
||||
|
||||
var settings = new DotNetCorePackSettings
|
||||
{
|
||||
@ -210,7 +215,10 @@ Task("ReleasePackagesToUnstableFeed")
|
||||
.IsDependentOn("CreatePackages")
|
||||
.Does(() =>
|
||||
{
|
||||
PublishPackages(nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl);
|
||||
if (ShouldPublishToUnstableFeed(nugetFeedUnstableBranchFilter, versioning.BranchName))
|
||||
{
|
||||
PublishPackages(packagesDir, artifactsFile, nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl);
|
||||
}
|
||||
});
|
||||
|
||||
Task("EnsureStableReleaseRequirements")
|
||||
@ -262,7 +270,7 @@ Task("ReleasePackagesToStableFeed")
|
||||
.IsDependentOn("DownloadGitHubReleaseArtifacts")
|
||||
.Does(() =>
|
||||
{
|
||||
PublishPackages(nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl);
|
||||
PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl);
|
||||
});
|
||||
|
||||
Task("Release")
|
||||
@ -271,21 +279,20 @@ Task("Release")
|
||||
RunTarget(target);
|
||||
|
||||
/// Gets nuique nuget version for this commit
|
||||
private string GetNuGetVersionForCommit()
|
||||
private GitVersion GetNuGetVersionForCommit()
|
||||
{
|
||||
GitVersion(new GitVersionSettings{
|
||||
UpdateAssemblyInfo = false,
|
||||
OutputType = GitVersionOutput.BuildServer
|
||||
});
|
||||
|
||||
var versionInfo = GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json });
|
||||
return versionInfo.NuGetVersion;
|
||||
return GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json });
|
||||
}
|
||||
|
||||
/// Updates project version in all of our projects
|
||||
private void PersistVersion(string version)
|
||||
private void PersistVersion(string committedVersion, string newVersion)
|
||||
{
|
||||
Information(string.Format("We'll search all project.json files for {0} and replace with {1}...", committedVersion, version));
|
||||
Information(string.Format("We'll search all project.json files for {0} and replace with {1}...", committedVersion, newVersion));
|
||||
|
||||
var projectJsonFiles = GetFiles("./**/project.json");
|
||||
|
||||
@ -296,24 +303,30 @@ private void PersistVersion(string version)
|
||||
Information(string.Format("Updating {0}...", file));
|
||||
|
||||
var updatedProjectJson = System.IO.File.ReadAllText(file)
|
||||
.Replace(committedVersion, version);
|
||||
.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()
|
||||
private void GenerateReleaseNotes(ConvertableFilePath file)
|
||||
{
|
||||
Information("Generating release notes at " + releaseNotesFile);
|
||||
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 " + releaseNotesFile });
|
||||
new ProcessSettings { Arguments = ". /o " + file });
|
||||
|
||||
if (string.IsNullOrEmpty(System.IO.File.ReadAllText(releaseNotesFile)))
|
||||
if (string.IsNullOrEmpty(System.IO.File.ReadAllText(file)))
|
||||
{
|
||||
System.IO.File.WriteAllText(releaseNotesFile, "No issues closed since last release");
|
||||
System.IO.File.WriteAllText(file, "No issues closed since last release");
|
||||
}
|
||||
|
||||
if (releaseNotesExitCode != 0)
|
||||
@ -323,7 +336,7 @@ private void GenerateReleaseNotes()
|
||||
}
|
||||
|
||||
/// Publishes code and symbols packages to nuget feed, based on contents of artifacts file
|
||||
private void PublishPackages(string feedApiKey, string codeFeedUrl, string symbolFeedUrl)
|
||||
private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFilePath artifactsFile, string feedApiKey, string codeFeedUrl, string symbolFeedUrl)
|
||||
{
|
||||
var artifacts = System.IO.File
|
||||
.ReadAllLines(artifactsFile)
|
||||
@ -332,8 +345,7 @@ private void PublishPackages(string feedApiKey, string codeFeedUrl, string symbo
|
||||
|
||||
var codePackage = packagesDir + File(artifacts["nuget"]);
|
||||
|
||||
Information("Pushing package");
|
||||
|
||||
Information("Pushing package " + codePackage);
|
||||
NuGetPush(
|
||||
codePackage,
|
||||
new NuGetPushSettings {
|
||||
@ -359,3 +371,18 @@ private string GetResource(string url)
|
||||
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;
|
||||
}
|
@ -28,6 +28,8 @@ namespace Ocelot.Configuration.Builder
|
||||
private ServiceProviderConfiguraion _serviceProviderConfiguraion;
|
||||
private bool _useQos;
|
||||
private QoSOptions _qosOptions;
|
||||
public bool _enableRateLimiting;
|
||||
public RateLimitOptions _rateLimitOptions;
|
||||
|
||||
public ReRouteBuilder WithLoadBalancer(string loadBalancer)
|
||||
{
|
||||
@ -160,6 +162,19 @@ namespace Ocelot.Configuration.Builder
|
||||
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(
|
||||
@ -184,7 +199,9 @@ namespace Ocelot.Configuration.Builder
|
||||
_loadBalancerKey,
|
||||
_serviceProviderConfiguraion,
|
||||
_useQos,
|
||||
_qosOptions);
|
||||
_qosOptions,
|
||||
_enableRateLimiting,
|
||||
_rateLimitOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +123,10 @@ namespace Ocelot.Configuration.Creator
|
||||
|
||||
var qosOptions = BuildQoSOptions(fileReRoute);
|
||||
|
||||
var enableRateLimiting = IsEnableRateLimiting(fileReRoute);
|
||||
|
||||
var rateLimitOption = BuildRateLimitOptions(fileReRoute, globalConfiguration, enableRateLimiting);
|
||||
|
||||
var reRoute = new ReRouteBuilder()
|
||||
.WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate)
|
||||
.WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate)
|
||||
@ -146,13 +150,34 @@ namespace Ocelot.Configuration.Creator
|
||||
.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()
|
||||
|
@ -6,10 +6,14 @@ namespace Ocelot.Configuration.File
|
||||
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; }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
39
src/Ocelot/Configuration/File/FileRateLimitOptions.cs
Normal file
39
src/Ocelot/Configuration/File/FileRateLimitOptions.cs
Normal file
@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.Configuration.File
|
||||
{
|
||||
public class FileRateLimitOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
|
||||
/// </summary>
|
||||
public string ClientIdHeader { get; set; } = "ClientId";
|
||||
|
||||
/// <summary>
|
||||
/// 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}
|
||||
/// </summary>
|
||||
public string QuotaExceededMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key
|
||||
/// </summary>
|
||||
public string RateLimitCounterPrefix { get; set; } = "ocelot";
|
||||
|
||||
/// <summary>
|
||||
/// Disables X-Rate-Limit and Rety-After headers
|
||||
/// </summary>
|
||||
public bool DisableRateLimitHeaders { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
|
||||
/// </summary>
|
||||
public int HttpStatusCode { get; set; } = 429;
|
||||
}
|
||||
|
||||
|
||||
}
|
34
src/Ocelot/Configuration/File/FileRateLimitRule.cs
Normal file
34
src/Ocelot/Configuration/File/FileRateLimitRule.cs
Normal file
@ -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<string>();
|
||||
}
|
||||
|
||||
public List<string> ClientWhitelist { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables endpoint rate limiting based URL path and HTTP verb
|
||||
/// </summary>
|
||||
public bool EnableRateLimiting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit period as in 1s, 1m, 1h
|
||||
/// </summary>
|
||||
public string Period { get; set; }
|
||||
|
||||
public double PeriodTimespan { get; set; }
|
||||
/// <summary>
|
||||
/// Maximum number of requests that a client can make in a defined period
|
||||
/// </summary>
|
||||
public long Limit { get; set; }
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ namespace Ocelot.Configuration.File
|
||||
AuthenticationOptions = new FileAuthenticationOptions();
|
||||
FileCacheOptions = new FileCacheOptions();
|
||||
QoSOptions = new FileQoSOptions();
|
||||
RateLimitOptions = new FileRateLimitRule();
|
||||
}
|
||||
|
||||
public string DownstreamPathTemplate { get; set; }
|
||||
@ -32,5 +33,6 @@ namespace Ocelot.Configuration.File
|
||||
public int DownstreamPort { get; set; }
|
||||
public FileQoSOptions QoSOptions { get; set; }
|
||||
public string LoadBalancer {get;set;}
|
||||
public FileRateLimitRule RateLimitOptions { get; set; }
|
||||
}
|
||||
}
|
83
src/Ocelot/Configuration/RateLimitOptions.cs
Normal file
83
src/Ocelot/Configuration/RateLimitOptions.cs
Normal file
@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// RateLimit Options
|
||||
/// </summary>
|
||||
public class RateLimitOptions
|
||||
{
|
||||
public RateLimitOptions(bool enbleRateLimiting, string clientIdHeader, List<string> clientWhitelist,bool disableRateLimitHeaders,
|
||||
string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode)
|
||||
{
|
||||
EnableRateLimiting = enbleRateLimiting;
|
||||
ClientIdHeader = clientIdHeader;
|
||||
ClientWhitelist = clientWhitelist?? new List<string>();
|
||||
DisableRateLimitHeaders = disableRateLimitHeaders;
|
||||
QuotaExceededMessage = quotaExceededMessage;
|
||||
RateLimitCounterPrefix = rateLimitCounterPrefix;
|
||||
RateLimitRule = rateLimitRule;
|
||||
HttpStatusCode = httpStatusCode;
|
||||
}
|
||||
|
||||
public RateLimitRule RateLimitRule { get; private set; }
|
||||
|
||||
public List<string> ClientWhitelist { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP header that holds the client identifier, by default is X-ClientId
|
||||
/// </summary>
|
||||
public string ClientIdHeader { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP Status code returned when rate limiting occurs, by default value is set to 429 (Too Many Requests)
|
||||
/// </summary>
|
||||
public int HttpStatusCode { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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}
|
||||
/// </summary>
|
||||
public string QuotaExceededMessage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the counter prefix, used to compose the rate limit counter cache key
|
||||
/// </summary>
|
||||
public string RateLimitCounterPrefix { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables endpoint rate limiting based URL path and HTTP verb
|
||||
/// </summary>
|
||||
public bool EnableRateLimiting { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Disables X-Rate-Limit and Rety-After headers
|
||||
/// </summary>
|
||||
public bool DisableRateLimitHeaders { get; private set; }
|
||||
}
|
||||
|
||||
public class RateLimitRule
|
||||
{
|
||||
public RateLimitRule(string period, TimeSpan periodTimespan, long limit)
|
||||
{
|
||||
Period = period;
|
||||
PeriodTimespan = periodTimespan;
|
||||
Limit = limit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate limit period as in 1s, 1m, 1h,1d
|
||||
/// </summary>
|
||||
public string Period { get; private set; }
|
||||
|
||||
public TimeSpan PeriodTimespan { get; private set; }
|
||||
/// <summary>
|
||||
/// Maximum number of requests that a client can make in a defined period
|
||||
/// </summary>
|
||||
public long Limit { get; private set; }
|
||||
}
|
||||
}
|
@ -27,7 +27,9 @@ namespace Ocelot.Configuration
|
||||
string reRouteKey,
|
||||
ServiceProviderConfiguraion serviceProviderConfiguraion,
|
||||
bool isQos,
|
||||
QoSOptions qos)
|
||||
QoSOptions qos,
|
||||
bool enableRateLimit,
|
||||
RateLimitOptions ratelimitOptions)
|
||||
{
|
||||
ReRouteKey = reRouteKey;
|
||||
ServiceProviderConfiguraion = serviceProviderConfiguraion;
|
||||
@ -54,6 +56,8 @@ namespace Ocelot.Configuration
|
||||
DownstreamScheme = downstreamScheme;
|
||||
IsQos = isQos;
|
||||
QosOptions = qos;
|
||||
EnableEndpointRateLimiting = enableRateLimit;
|
||||
RateLimitOptions = ratelimitOptions;
|
||||
}
|
||||
|
||||
public string ReRouteKey {get;private set;}
|
||||
@ -78,5 +82,7 @@ namespace Ocelot.Configuration
|
||||
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; }
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@ using Ocelot.Requester.QoS;
|
||||
using Ocelot.Responder;
|
||||
using Ocelot.ServiceDiscovery;
|
||||
using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider;
|
||||
using Ocelot.RateLimit;
|
||||
|
||||
namespace Ocelot.DependencyInjection
|
||||
{
|
||||
@ -133,12 +134,13 @@ namespace Ocelot.DependencyInjection
|
||||
services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
|
||||
services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>();
|
||||
services.AddSingleton<IAuthenticationHandlerCreator, AuthenticationHandlerCreator>();
|
||||
services.AddSingleton<IRateLimitCounterHandler, MemoryCacheRateLimitCounterHandler>();
|
||||
|
||||
// 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<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddScoped<IRequestScopedDataRepository, HttpDataRepository>();
|
||||
|
||||
services.AddMemoryCache();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
@ -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<ConfigurationBuilderCachePart> settings)
|
||||
{
|
||||
var cacheManagerOutputCache = CacheFactory.Build<HttpResponseMessage>("OcelotOutputCache", settings);
|
||||
var ocelotCacheManager = new OcelotCacheManagerCache<HttpResponseMessage>(cacheManagerOutputCache);
|
||||
services.AddSingleton<ICacheManager<HttpResponseMessage>>(cacheManagerOutputCache);
|
||||
services.AddSingleton<IOcelotCache<HttpResponseMessage>>(ocelotCacheManager);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddOcelot(this IServiceCollection services, IConfigurationRoot configurationRoot)
|
||||
{
|
||||
services.Configure<FileConfiguration>(configurationRoot);
|
||||
services.AddSingleton<IOcelotConfigurationCreator, FileOcelotConfigurationCreator>();
|
||||
services.AddSingleton<IOcelotConfigurationRepository, InMemoryOcelotConfigurationRepository>();
|
||||
services.AddSingleton<IConfigurationValidator, FileConfigurationValidator>();
|
||||
services.AddSingleton<IBaseUrlFinder, BaseUrlFinder>();
|
||||
|
||||
var identityServerConfiguration = IdentityServerConfigurationCreator.GetIdentityServerConfiguration();
|
||||
|
||||
if(identityServerConfiguration != null)
|
||||
{
|
||||
services.AddSingleton<IIdentityServerConfiguration>(identityServerConfiguration);
|
||||
services.AddSingleton<IHashMatcher, HashMatcher>();
|
||||
services.AddIdentityServer()
|
||||
.AddTemporarySigningCredential()
|
||||
.AddInMemoryApiResources(new List<ApiResource>
|
||||
{
|
||||
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<Secret>
|
||||
{
|
||||
new Secret
|
||||
{
|
||||
Value = identityServerConfiguration.ApiSecret.Sha256()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.AddInMemoryClients(new List<Client>
|
||||
{
|
||||
new Client
|
||||
{
|
||||
ClientId = identityServerConfiguration.ApiName,
|
||||
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
|
||||
ClientSecrets = new List<Secret> {new Secret(identityServerConfiguration.ApiSecret.Sha256())},
|
||||
AllowedScopes = identityServerConfiguration.AllowedScopes,
|
||||
AccessTokenType = identityServerConfiguration.AccessTokenType,
|
||||
Enabled = identityServerConfiguration.Enabled,
|
||||
RequireClientSecret = identityServerConfiguration.RequireClientSecret
|
||||
}
|
||||
}).AddResourceOwnerValidator<OcelotResourceOwnerPasswordValidator>();
|
||||
}
|
||||
|
||||
services.AddMvcCore()
|
||||
.AddAuthorization()
|
||||
.AddJsonFormatters();
|
||||
services.AddLogging();
|
||||
services.AddSingleton<IFileConfigurationRepository, FileConfigurationRepository>();
|
||||
services.AddSingleton<IFileConfigurationSetter, FileConfigurationSetter>();
|
||||
services.AddSingleton<IFileConfigurationProvider, FileConfigurationProvider>();
|
||||
services.AddSingleton<IQosProviderHouse, QosProviderHouse>();
|
||||
services.AddSingleton<IQoSProviderFactory, QoSProviderFactory>();
|
||||
services.AddSingleton<IServiceDiscoveryProviderFactory, ServiceDiscoveryProviderFactory>();
|
||||
services.AddSingleton<ILoadBalancerFactory, LoadBalancerFactory>();
|
||||
services.AddSingleton<ILoadBalancerHouse, LoadBalancerHouse>();
|
||||
services.AddSingleton<IOcelotLoggerFactory, AspDotNetLoggerFactory>();
|
||||
services.AddSingleton<IUrlBuilder, UrlBuilder>();
|
||||
services.AddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>();
|
||||
services.AddSingleton<IOcelotConfigurationProvider, OcelotConfigurationProvider>();
|
||||
services.AddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>();
|
||||
services.AddSingleton<IAuthoriser, ClaimsAuthoriser>();
|
||||
services.AddSingleton<IAddClaimsToRequest, AddClaimsToRequest>();
|
||||
services.AddSingleton<IAddHeadersToRequest, AddHeadersToRequest>();
|
||||
services.AddSingleton<IAddQueriesToRequest, AddQueriesToRequest>();
|
||||
services.AddSingleton<IClaimsParser, ClaimsParser>();
|
||||
services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>();
|
||||
services.AddSingleton<IUrlPathPlaceholderNameAndValueFinder, UrlPathPlaceholderNameAndValueFinder>();
|
||||
services.AddSingleton<IDownstreamPathPlaceholderReplacer, DownstreamTemplatePathPlaceholderReplacer>();
|
||||
services.AddSingleton<IDownstreamRouteFinder, DownstreamRouteFinder.Finder.DownstreamRouteFinder>();
|
||||
services.AddSingleton<IHttpRequester, HttpClientHttpRequester>();
|
||||
services.AddSingleton<IHttpResponder, HttpContextResponder>();
|
||||
services.AddSingleton<IRequestCreator, HttpRequestCreator>();
|
||||
services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
|
||||
services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>();
|
||||
services.AddSingleton<IAuthenticationHandlerCreator, AuthenticationHandlerCreator>();
|
||||
services.AddSingleton<IRateLimitCounterHandler, MemoryCacheRateLimitCounterHandler>();
|
||||
|
||||
// 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<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddScoped<IRequestScopedDataRepository, HttpDataRepository>();
|
||||
services.AddMemoryCache();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ using Ocelot.Request.Middleware;
|
||||
using Ocelot.Requester.Middleware;
|
||||
using Ocelot.RequestId.Middleware;
|
||||
using Ocelot.Responder.Middleware;
|
||||
using Ocelot.RateLimit.Middleware;
|
||||
|
||||
namespace Ocelot.Middleware
|
||||
{
|
||||
@ -64,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();
|
||||
|
||||
|
37
src/Ocelot/RateLimit/ClientRateLimitProcessor.cs
Normal file
37
src/Ocelot/RateLimit/ClientRateLimitProcessor.cs
Normal file
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
18
src/Ocelot/RateLimit/ClientRequestIdentity.cs
Normal file
18
src/Ocelot/RateLimit/ClientRequestIdentity.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
@ -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<RateLimitCounter>(stored);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Remove(string id)
|
||||
{
|
||||
_memoryCache.Remove(id);
|
||||
}
|
||||
}
|
||||
}
|
15
src/Ocelot/RateLimit/IRateLimitCounterHandler.cs
Normal file
15
src/Ocelot/RateLimit/IRateLimitCounterHandler.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
45
src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs
Normal file
45
src/Ocelot/RateLimit/MemoryCacheRateLimitCounterHandler.cs
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
138
src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs
Normal file
138
src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs
Normal file
@ -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<ClientRateLimitMiddleware>();
|
||||
_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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<ClientRateLimitMiddleware>();
|
||||
}
|
||||
}
|
||||
}
|
125
src/Ocelot/RateLimit/RateLimitCore.cs
Normal file
125
src/Ocelot/RateLimit/RateLimitCore.cs
Normal file
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
23
src/Ocelot/RateLimit/RateLimitCounter.cs
Normal file
23
src/Ocelot/RateLimit/RateLimitCounter.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ocelot.RateLimit
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the initial access time and the numbers of calls made from that point
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
27
src/Ocelot/RateLimit/RateLimitHeaders.cs
Normal file
27
src/Ocelot/RateLimit/RateLimitHeaders.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
@ -1,5 +1,13 @@
|
||||
{
|
||||
"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",
|
||||
|
88
src/Ocelot/project.json.orig
Normal file
88
src/Ocelot/project.json.orig
Normal file
@ -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": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
165
test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs
Normal file
165
test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs
Normal file
@ -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<FileReRoute>
|
||||
{
|
||||
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<string>(),
|
||||
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<FileReRoute>
|
||||
{
|
||||
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<string>() { "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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -238,6 +238,17 @@ namespace Ocelot.AcceptanceTests
|
||||
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);
|
||||
@ -265,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();
|
||||
|
305
test/Ocelot.AcceptanceTests/Steps.cs.orig
Normal file
305
test/Ocelot.AcceptanceTests/Steps.cs.orig
Normal file
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void GivenOcelotIsRunning()
|
||||
{
|
||||
_webHostBuilder = new WebHostBuilder();
|
||||
|
||||
_webHostBuilder.ConfigureServices(s =>
|
||||
{
|
||||
s.AddSingleton(_webHostBuilder);
|
||||
});
|
||||
|
||||
_ocelotServer = new TestServer(_webHostBuilder
|
||||
.UseStartup<Startup>());
|
||||
|
||||
_ocelotClient = _ocelotServer.CreateClient();
|
||||
}
|
||||
|
||||
internal void ThenTheResponseShouldBe(FileConfiguration expected)
|
||||
{
|
||||
var response = JsonConvert.DeserializeObject<FileConfiguration>(_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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<ConfigurationBuilderCachePart> 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<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("client_id", "client"),
|
||||
new KeyValuePair<string, string>("client_secret", "secret"),
|
||||
new KeyValuePair<string, string>("scope", "api"),
|
||||
new KeyValuePair<string, string>("username", "test"),
|
||||
new KeyValuePair<string, string>("password", "test"),
|
||||
new KeyValuePair<string, string>("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<BearerToken>(responseContent);
|
||||
}
|
||||
}
|
||||
|
||||
public void GivenIHaveAnOcelotToken(string adminPath)
|
||||
{
|
||||
var tokenUrl = $"{adminPath}/connect/token";
|
||||
var formData = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new KeyValuePair<string, string>("client_id", "admin"),
|
||||
new KeyValuePair<string, string>("client_secret", "secret"),
|
||||
new KeyValuePair<string, string>("scope", "admin"),
|
||||
new KeyValuePair<string, string>("username", "admin"),
|
||||
new KeyValuePair<string, string>("password", "admin"),
|
||||
new KeyValuePair<string, string>("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<BearerToken>(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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Ocelot.AcceptanceTests
|
||||
{
|
||||
public static class TestConfiguration
|
||||
{
|
||||
public static string ConfigurationPath => $"{AppContext.BaseDirectory}/configuration.json";
|
||||
public static string ConfigurationPath => Path.Combine(AppContext.BaseDirectory, "configuration.json");
|
||||
}
|
||||
}
|
||||
|
17
test/Ocelot.AcceptanceTests/TestConfiguration.cs.orig
Normal file
17
test/Ocelot.AcceptanceTests/TestConfiguration.cs.orig
Normal file
@ -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
|
||||
}
|
||||
}
|
1
test/Ocelot.AcceptanceTests/configuration.json
Executable file
1
test/Ocelot.AcceptanceTests/configuration.json
Executable file
@ -0,0 +1 @@
|
||||
{"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}}}
|
@ -23,23 +23,25 @@
|
||||
"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.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"
|
||||
"Consul": "0.7.2.1",
|
||||
"Microsoft.Extensions.Caching.Memory": "1.1.0",
|
||||
"xunit": "2.2.0-rc1-build3507"
|
||||
},
|
||||
"runtimes": {
|
||||
"win10-x64": {},
|
||||
"osx.10.11-x64": {},
|
||||
"osx.10.12-x64": {},
|
||||
"win7-x64": {}
|
||||
"osx.10.11-x64":{},
|
||||
"osx.10.12-x64":{},
|
||||
"win7-x64": {},
|
||||
"win10-x64": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.1": {
|
||||
|
59
test/Ocelot.AcceptanceTests/project.json.orig
Normal file
59
test/Ocelot.AcceptanceTests/project.json.orig
Normal file
@ -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": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -9,10 +9,10 @@
|
||||
"BenchmarkDotNet": "0.10.2"
|
||||
},
|
||||
"runtimes": {
|
||||
"win10-x64": {},
|
||||
"osx.10.11-x64":{},
|
||||
"osx.10.12-x64": {},
|
||||
"win7-x64": {}
|
||||
"osx.10.12-x64":{},
|
||||
"win7-x64": {},
|
||||
"win10-x64": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.1": {
|
||||
|
28
test/Ocelot.Benchmarks/project.json.orig
Normal file
28
test/Ocelot.Benchmarks/project.json.orig
Normal file
@ -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": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
{"ReRoutes":[],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"AdministrationPath":"/administration"}}
|
||||
{"ReRoutes":[],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"AdministrationPath":"/administration","RateLimitOptions":{"ClientIdHeader":"ClientId","QuotaExceededMessage":null,"RateLimitCounterPrefix":"ocelot","DisableRateLimitHeaders":false,"HttpStatusCode":429}}}
|
@ -37,7 +37,6 @@ namespace Ocelot.ManualTest
|
||||
})
|
||||
.WithDictionaryHandle();
|
||||
};
|
||||
|
||||
services.AddOcelotOutputCaching(settings);
|
||||
services.AddOcelot(Configuration);
|
||||
}
|
||||
|
@ -47,9 +47,9 @@
|
||||
},
|
||||
{
|
||||
"DownstreamPathTemplate": "/posts",
|
||||
"DownstreamScheme": "http",
|
||||
"DownstreamScheme": "https",
|
||||
"DownstreamHost": "jsonplaceholder.typicode.com",
|
||||
"DownstreamPort": 80,
|
||||
"DownstreamPort": 443,
|
||||
"UpstreamPathTemplate": "/posts",
|
||||
"UpstreamHttpMethod": "Get",
|
||||
"QoSOptions": {
|
||||
|
310
test/Ocelot.ManualTest/configuration.json.orig
Normal file
310
test/Ocelot.ManualTest/configuration.json.orig
Normal file
@ -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"
|
||||
}
|
||||
}
|
@ -22,10 +22,10 @@
|
||||
"Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"
|
||||
},
|
||||
"runtimes": {
|
||||
"win10-x64": {},
|
||||
"osx.10.11-x64":{},
|
||||
"osx.10.12-x64": {},
|
||||
"win7-x64": {}
|
||||
"osx.10.12-x64":{},
|
||||
"win7-x64": {},
|
||||
"win10-x64": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.1": {
|
||||
|
72
test/Ocelot.ManualTest/project.json.orig
Normal file
72
test/Ocelot.ManualTest/project.json.orig
Normal file
@ -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%" ]
|
||||
}
|
||||
}
|
@ -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<IRequestScopedDataRepository> _scopedRepository;
|
||||
private readonly string _url;
|
||||
private readonly TestServer _server;
|
||||
private readonly HttpClient _client;
|
||||
private OkResponse<DownstreamRoute> _downstreamRoute;
|
||||
private int responseStatusCode;
|
||||
|
||||
public ClientRateLimitMiddlewareTests()
|
||||
{
|
||||
_url = "http://localhost:51879/api/ClientRateLimit";
|
||||
_scopedRepository = new Mock<IRequestScopedDataRepository>();
|
||||
var builder = new WebHostBuilder()
|
||||
.ConfigureServices(x =>
|
||||
{
|
||||
x.AddSingleton<IOcelotLoggerFactory, AspDotNetLoggerFactory>();
|
||||
x.AddLogging();
|
||||
x.AddMemoryCache();
|
||||
x.AddSingleton<IRateLimitCounterHandler, MemoryCacheRateLimitCounterHandler>();
|
||||
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<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
|
||||
new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
|
||||
new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List<string>(), 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<Ocelot.DownstreamRouteFinder.UrlMatcher.UrlPathPlaceholderNameAndValue>(),
|
||||
new ReRouteBuilder().WithEnableRateLimiting(true).WithRateLimitOptions(
|
||||
new Ocelot.Configuration.RateLimitOptions(true, "ClientId", new List<string>() { "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>(downstreamRoute);
|
||||
_scopedRepository
|
||||
.Setup(x => x.Get<DownstreamRoute>(It.IsAny<string>()))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,7 +4,15 @@
|
||||
"testRunner": "xunit",
|
||||
|
||||
"dependencies": {
|
||||
"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",
|
||||
@ -12,26 +20,18 @@
|
||||
"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",
|
||||
"xunit": "2.2.0-beta2-build3300",
|
||||
"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",
|
||||
"Moq": "4.6.38-alpha",
|
||||
"Ocelot": "0.0.0-dev",
|
||||
"Shouldly": "2.8.2",
|
||||
"TestStack.BDDfy": "4.3.2",
|
||||
"Microsoft.AspNetCore.Authentication.OAuth": "1.1.0",
|
||||
"Microsoft.DotNet.InternalAbstractions": "1.0.0",
|
||||
"Microsoft.AspNetCore.Cryptography.KeyDerivation": "1.1.0"
|
||||
"xunit": "2.2.0-rc1-build3507"
|
||||
},
|
||||
"runtimes": {
|
||||
"win10-x64": {},
|
||||
"osx.10.11-x64":{},
|
||||
"osx.10.12-x64": {},
|
||||
"win7-x64": {}
|
||||
"osx.10.12-x64":{},
|
||||
"win7-x64": {},
|
||||
"win10-x64": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.1": {
|
||||
|
50
test/Ocelot.UnitTests/project.json.orig
Normal file
50
test/Ocelot.UnitTests/project.json.orig
Normal file
@ -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": [
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user