Merge branch 'develop' of https://github.com/geffzhang/Ocelot into develop

This commit is contained in:
geffzhang 2017-02-26 14:06:28 +08:00
commit 4bd14f7537
234 changed files with 10991 additions and 1221 deletions

7
.gitignore vendored
View File

@ -235,3 +235,10 @@ _Pvt_Extensions
# FAKE - F# Make # FAKE - F# Make
.fake/ .fake/
tools/
# MacOS
.DS_Store
# Ocelot acceptance test config
test/Ocelot.AcceptanceTests/configuration.json

4
GitVersion.yml Normal file
View File

@ -0,0 +1,4 @@
mode: ContinuousDelivery
branches: {}
ignore:
sha: []

View File

@ -1,44 +0,0 @@
<?xml version="1.0"?>
<package >
<metadata>
<id>Ocelot</id>
<version>1.0.0</version>
<authors>Tom Pallister</authors>
<owners>Tom Pallister</owners>
<licenseUrl>https://github.com/TomPallister/Ocelot/blob/develop/LICENSE.md</licenseUrl>
<projectUrl>https://github.com/TomPallister/Ocelot</projectUrl>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Ocelot Api Gateway</description>
<releaseNotes>Latest Ocelot</releaseNotes>
<copyright></copyright>
<tags></tags>
<dependencies>
<dependency id="Microsoft.AspNetCore.Mvc" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Server.IISIntegration" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Server.Kestrel" version="1.0.0" />
<dependency id="Microsoft.Extensions.Configuration.EnvironmentVariables" version="1.0.0" />
<dependency id="Microsoft.Extensions.Configuration.FileExtensions" version="1.0.0" />
<dependency id="Microsoft.Extensions.Configuration.Json" version="1.0.0" />
<dependency id="Microsoft.Extensions.Logging" version="1.0.0" />
<dependency id="Microsoft.Extensions.Logging.Console" version="1.0.0" />
<dependency id="Microsoft.Extensions.Logging.Debug" version="1.0.0" />
<dependency id="Microsoft.Extensions.Options.ConfigurationExtensions" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Http" version="1.0.0" />
<dependency id="System.Text.RegularExpressions" version="4.1.0" />
<dependency id="Microsoft.AspNetCore.Authentication.OAuth" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Authentication.JwtBearer" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Authentication.OpenIdConnect" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Authentication.Cookies" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Authentication.Google" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Authentication.Facebook" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Authentication.Twitter" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Authentication.MicrosoftAccount" version="1.0.0" />
<dependency id="Microsoft.AspNetCore.Authentication" version="1.0.0" />
<dependency id="IdentityServer4.AccessTokenValidation" version="1.0.1-rc2" />
</dependencies>
</metadata>
<files>
<file src="src\Ocelot\bin\Release\netcoreapp1.4\Ocelot.dll" target="lib\netstandard1.4" />
<file src="src\Ocelot\bin\Release\netcoreapp1.4\Ocelot.pdb" target="lib\netstandard1.4" />
</files>
</package>

View File

@ -8,17 +8,24 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore .gitignore = .gitignore
appveyor.yml = appveyor.yml build-and-release-unstable.ps1 = build-and-release-unstable.ps1
build-and-run-tests.bat = build-and-run-tests.bat build-and-run-tests.ps1 = build-and-run-tests.ps1
build.bat = build.bat build.cake = build.cake
build.ps1 = build.ps1
build.readme.md = build.readme.md
configuration-explanation.txt = configuration-explanation.txt configuration-explanation.txt = configuration-explanation.txt
configuration.yaml = configuration.yaml
GitVersion.yml = GitVersion.yml
global.json = global.json global.json = global.json
LICENSE.md = LICENSE.md LICENSE.md = LICENSE.md
Ocelot.nuspec = Ocelot.nuspec ocelot.postman_collection.json = ocelot.postman_collection.json
push-to-nuget.bat = push-to-nuget.bat
README.md = README.md README.md = README.md
run-benchmarks.bat = run-benchmarks.bat release.ps1 = release.ps1
run-tests.bat = run-tests.bat ReleaseNotes.md = ReleaseNotes.md
run-acceptance-tests.ps1 = run-acceptance-tests.ps1
run-benchmarks.ps1 = run-benchmarks.ps1
run-unit-tests.ps1 = run-unit-tests.ps1
version.ps1 = version.ps1
EndProjectSection EndProjectSection
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot", "src\Ocelot\Ocelot.xproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot", "src\Ocelot\Ocelot.xproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}"
@ -33,6 +40,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.ManualTest", "test\O
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.Benchmarks", "test\Ocelot.Benchmarks\Ocelot.Benchmarks.xproj", "{106B49E6-95F6-4A7B-B81C-96BFA74AF035}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.Benchmarks", "test\Ocelot.Benchmarks\Ocelot.Benchmarks.xproj", "{106B49E6-95F6-4A7B-B81C-96BFA74AF035}"
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.IntegrationTests", "test\Ocelot.IntegrationTests\Ocelot.IntegrationTests.xproj", "{D4575572-99CA-4530-8737-C296EDA326F8}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -59,6 +68,10 @@ Global
{106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.Build.0 = Debug|Any CPU {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.Build.0 = Debug|Any CPU
{106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.ActiveCfg = Release|Any CPU {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.ActiveCfg = Release|Any CPU
{106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.Build.0 = Release|Any CPU {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Release|Any CPU.Build.0 = Release|Any CPU
{D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4575572-99CA-4530-8737-C296EDA326F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4575572-99CA-4530-8737-C296EDA326F8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -69,5 +82,6 @@ Global
{F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {5B401523-36DA-4491-B73A-7590A26E420B} {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52} = {5B401523-36DA-4491-B73A-7590A26E420B}
{02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B} {02BBF4C5-517E-4157-8D21-4B8B9E118B7A} = {5B401523-36DA-4491-B73A-7590A26E420B}
{106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B} {106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B}
{D4575572-99CA-4530-8737-C296EDA326F8} = {5B401523-36DA-4491-B73A-7590A26E420B}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

266
README.md
View File

@ -1,6 +1,6 @@
# Ocelot # Ocelot
[![Build status](https://ci.appveyor.com/api/projects/status/roahbe4nl526ysya?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot) [![Build status](https://ci.appveyor.com/api/projects/status/r6sv51qx36sis1je?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb)
[![Join the chat at https://gitter.im/Ocelotey/Lobby](https://badges.gitter.im/Ocelotey/Lobby.svg)](https://gitter.im/Ocelotey/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/Ocelotey/Lobby](https://badges.gitter.im/Ocelotey/Lobby.svg)](https://gitter.im/Ocelotey/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@ -30,9 +30,6 @@ and retrived as the requests goes back up the Ocelot pipeline. There is a piece
that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client. that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client.
That is basically it with a bunch of other features. 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 ## Contributing
Pull requests, issues and commentary welcome! No special process just create a request and get in Pull requests, issues and commentary welcome! No special process just create a request and get in
@ -41,13 +38,13 @@ touch either via gitter or create an issue.
## How to install ## How to install
Ocelot is designed to work with ASP.NET core only and is currently Ocelot is designed to work with ASP.NET core only and is currently
built to netcoreapp1.4 [this](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) documentation may prove helpful when working out if Ocelot would be suitable for you. built to netcoreapp1.1 [this](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) documentation may prove helpful when working out if Ocelot would be suitable for you.
Install Ocelot and it's dependecies using nuget. At the moment Install Ocelot and it's dependecies using nuget. At the moment
all we have is the pre version. Once we have something working in all we have is the pre version. Once we have something working in
a half decent way we will drop a version. a half decent way we will drop a version.
`Install-Package Ocelot -Pre` `Install-Package Ocelot`
All versions can be found [here](https://www.nuget.org/packages/Ocelot/) All versions can be found [here](https://www.nuget.org/packages/Ocelot/)
@ -61,11 +58,12 @@ The ReRoutes are the objects that tell Ocelot how to treat an upstream request.
configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful 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. if you don't want to manage lots of ReRoute specific settings.
{ ```json
{
"ReRoutes": [], "ReRoutes": [],
"GlobalConfiguration": {} "GlobalConfiguration": {}
} }
```
More information on how to use these options is below.. More information on how to use these options is below..
## Startup ## Startup
@ -73,8 +71,9 @@ More information on how to use these options is below..
An example startup using a json file for configuration can be seen 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. Currently this is the only way to get configuration into Ocelot.
public class Startup ```csharp
{ public class Startup
{
public Startup(IHostingEnvironment env) public Startup(IHostingEnvironment env)
{ {
var builder = new ConfigurationBuilder() var builder = new ConfigurationBuilder()
@ -101,18 +100,46 @@ Currently this is the only way to get configuration into Ocelot.
}; };
services.AddOcelotOutputCaching(settings); services.AddOcelotOutputCaching(settings);
services.AddOcelotFileConfiguration(Configuration); services.AddOcelot(Configuration);
services.AddOcelot();
} }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) public async void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{ {
loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddConsole(Configuration.GetSection("Logging"));
app.UseOcelot(); await app.UseOcelot();
} }
} }
}
```
Then in your Program.cs you will want to have the following. This can be changed if you
don't wan't to use the default url e.g. UseUrls(someUrls) and should work as long as you keep the WebHostBuilder registration.
```csharp
public class Program
{
public static void Main(string[] args)
{
IWebHostBuilder builder = new WebHostBuilder();
builder.ConfigureServices(s => {
s.AddSingleton(builder);
});
builder.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<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.
This is pretty much all you need to get going.......more to come! This is pretty much all you need to get going.......more to come!
@ -122,50 +149,141 @@ Ocelot's primary functionality is to take incomeing http requests and forward th
to a downstream service. At the moment in the form of another http request (in the future to a downstream service. At the moment in the form of another http request (in the future
this could be any transport mechanism.). this could be any transport mechanism.).
Ocelot always adds a trailing slash to an UpstreamTemplate. Ocelot always adds a trailing slash to an UpstreamPathTemplate.
Ocelot's describes the routing of one request to another as a ReRoute. In order to get 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. anything working in Ocelot you need to set up a ReRoute in the configuration.
{ ```json
{
"ReRoutes": [ "ReRoutes": [
] ]
} }
```
In order to set up a ReRoute you need to add one to the json array called ReRoutes like In order to set up a ReRoute you need to add one to the json array called ReRoutes like
the following. the following.
{ ```json
"DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts/{postId}", {
"UpstreamTemplate": "/posts/{postId}", "DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamScheme": "https",
"DownstreamPort": 80,
"DownstreamHost" "localhost"
"UpstreamPathTemplate": "/posts/{postId}",
"UpstreamHttpMethod": "Put" "UpstreamHttpMethod": "Put"
} }
```
The DownstreamTemplate is the URL that this request will be forwarded to. The DownstreamPathTemplate,Scheme, Port and Host make the URL that this request will be forwarded to.
The UpstreamTemplate is the URL that Ocelot will use to identity which The UpstreamPathTemplate is the URL that Ocelot will use to identity which
DownstreamTemplate to use for a given request. Finally the UpstreamHttpMethod is used so 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 :) 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}. In Ocelot you can add placeholders for variables to your Templates in the form of {something}.
The placeholder needs to be in both the DownstreamTemplate and UpstreamTemplate. If it is The placeholder needs to be in both the DownstreamPathTemplate and UpstreamPathTemplate. If it is
Ocelot will attempt to replace the placeholder with the correct variable value from the Ocelot will attempt to replace the placeholder with the correct variable value from the
Upstream URL when the request comes in. Upstream URL when the request comes in.
At the moment without any configuration Ocelot will default to all ReRoutes being case insensitive. 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. 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 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 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! the ReRoute to be case sensitive is my advice!
## Administration
Ocelot supports changing configuration during runtime via an authenticated HTTP API. The API is authenticated
using bearer tokens that you request from iteself. This is provided by the amazing [IdentityServer](https://github.com/IdentityServer/IdentityServer4)
project that I have been using for a few years now. Check them out.
In order to enable the administration section you need to do a few things. First of all add this to your
initial configuration.json. The value can be anything you want and it is obviously reccomended don't use
a url you would like to route through with Ocelot as this will not work. The administration uses the
MapWhen functionality of asp.net core and all requests to root/administration will be sent there not
to the Ocelot middleware.
```json
"GlobalConfiguration": {
"AdministrationPath": "/administration"
}
```
This will get the admin area set up but not the authentication. Please note that this is a very basic approach to
this problem and if needed we can obviously improve on this!
You need to set 3 environmental variables.
OCELOT_USERNAME
OCELOT_HASH
OCELOT_SALT
These need to be the admin username you want to use with Ocelot and the hash and salt of the password you want to
use given hashing algorythm. When requesting bearer tokens for use with the administration api you will need to
supply username and password.
In order to create a hash and salt of your password please check out HashCreationTests.should_create_hash_and_salt()
this technique is based on [this](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing)
using SHA256 rather than SHA1.
Now if you went with the configuration options above and want to access the API you can use the postman scripts
called ocelot.postman_collection.json in the solution to change the Ocelot configuration. Obviously these
will need to be changed if you are running Ocelot on a different url to http://localhost:5000.
The scripts show you how to request a bearer token from ocelot and then use it to GET the existing configuration and POST
a configuration.
## Service Discovery
Ocelot allows you to specify a service discovery provider and will use this to find the host and port
for the downstream service Ocelot is forwarding a request to. At the moment this is only supported in the
GlobalConfiguration section which means the same service discovery provider will be used for all ReRoutes
you specify a ServiceName for at ReRoute level.
In the future we can add a feature that allows ReRoute specfic configuration.
At the moment the only supported service discovery provider is Consul. The following is required in the
GlobalConfiguration. The Provider is required and if you do not specify a host and port the Consul default
will be used.
```json
"ServiceDiscoveryProvider": {
"Provider":"Consul",
"Host":"localhost",
"Port":8500
}
```
In order to tell Ocelot a ReRoute is to use the service discovery provider for its host and port you must add the
ServiceName and load balancer you wish to use when making requests downstream. At the moment Ocelot has a RoundRobin
and LeastConnection algorithm you can use. If no load balancer is specified Ocelot will not load balance requests.
```json
{
"DownstreamPathTemplate": "/api/posts/{postId}",
"DownstreamScheme": "https",
"UpstreamPathTemplate": "/posts/{postId}",
"UpstreamHttpMethod": "Put",
"ServiceName": "product",
"LoadBalancer": "LeastConnection"
}
```
When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balancer
requests across any available services.
## Authentication ## Authentication
Ocelot currently supports the use of bearer tokens with Identity Server (more providers to 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 come if required). In order to identity a ReRoute as authenticated it needs the following
configuration added. configuration added.
"AuthenticationOptions": { ```json
"AuthenticationOptions": {
"Provider": "IdentityServer", "Provider": "IdentityServer",
"ProviderRootUrl": "http://localhost:52888", "ProviderRootUrl": "http://localhost:52888",
"ScopeName": "api", "ScopeName": "api",
@ -174,7 +292,8 @@ configuration added.
"offline_access" "offline_access"
], ],
"ScopeSecret": "secret" "ScopeSecret": "secret"
} }
```
In this example the Provider is specified as IdentityServer. This string is important 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 because it is used to identity the authentication provider (as previously mentioned in
@ -193,9 +312,11 @@ is 401 unauthorised.
Ocelot supports claims based authorisation which is run post authentication. This means if 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. you have a route you want to authorise you can add the following to you ReRoute configuration.
"RouteClaimsRequirement": { ```json
"RouteClaimsRequirement": {
"UserType": "registered" "UserType": "registered"
}, },
```
In this example when the authorisation middleware is called Ocelot will check to see 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 the user has the claim type UserType and if the value of that claim is registered.
@ -234,10 +355,12 @@ and add whatever was at the index requested to the transform.
Below is an example configuration that will transforms claims to claims Below is an example configuration that will transforms claims to claims
```json
"AddClaimsToRequest": { "AddClaimsToRequest": {
"UserType": "Claims[sub] > value[0] > |", "UserType": "Claims[sub] > value[0] > |",
"UserId": "Claims[sub] > value[1] > |" "UserId": "Claims[sub] > value[1] > |"
}, },
```
This shows a transforms where Ocelot looks at the users sub claim and transforms it into 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". UserType and UserId claims. Assuming the sub looks like this "usertypevalue|useridvalue".
@ -246,9 +369,11 @@ UserType and UserId claims. Assuming the sub looks like this "usertypevalue|user
Below is an example configuration that will transforms claims to headers Below is an example configuration that will transforms claims to headers
"AddHeadersToRequest": { ```json
"AddHeadersToRequest": {
"CustomerId": "Claims[sub] > value[1] > |" "CustomerId": "Claims[sub] > value[1] > |"
}, },
```
This shows a transform where Ocelot looks at the users sub claim and trasnforms it into a 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". CustomerId header. Assuming the sub looks like this "usertypevalue|useridvalue".
@ -257,26 +382,36 @@ CustomerId header. Assuming the sub looks like this "usertypevalue|useridvalue".
Below is an example configuration that will transforms claims to query string parameters Below is an example configuration that will transforms claims to query string parameters
"AddQueriesToRequest": { ```json
"AddQueriesToRequest": {
"LocationId": "Claims[LocationId] > value", "LocationId": "Claims[LocationId] > value",
}, },
```
This shows a transform where Ocelot looks at the users LocationId claim and add its as 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. a query string parameter to be forwarded onto the downstream service.
## Logging ## Quality of Service
Ocelot uses the standard logging interfaces ILoggerFactory / ILogger<T> at the moment. Ocelot supports one QoS capability at the current time. You can set on a per ReRoute basis if you
This is encapsulated in IOcelotLogger / IOcelotLoggerFactory with an implementation want to use a circuit breaker when making requests to a downstream service. This uses the an awesome
for the standard asp.net core logging stuff at the moment. .NET library called Polly check them out [here](https://github.com/App-vNext/Polly).
There are a bunch of debugging logs in the ocelot middlewares however I think the Add the following section to a ReRoute configuration.
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 ```json
work out how to override the request id that get's logged when setting IncludeScopes "QoSOptions": {
to true for logging settings. Nicely onto the next feature. "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 ## RequestId / CorrelationId
@ -291,14 +426,18 @@ have an OcelotRequestId.
In order to use the requestid feature in your ReRoute configuration add this setting 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. 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 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. 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. It behaves in exactly the same way as the ReRoute level RequestIdKey settings.
@ -317,7 +456,9 @@ and setting a TTL in seconds to expire the cache. More to come!
In orde to use caching on a route in your ReRoute configuration add this setting. 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. In this example ttl seconds is set to 15 which means the cache will expire after 15 seconds.
@ -329,15 +470,17 @@ pipeline and you are using any of the following. Remove them and try again!
When setting up Ocelot in your Startup.cs you can provide some additonal middleware When setting up Ocelot in your Startup.cs you can provide some additonal middleware
and override middleware. This is done as follos. and override middleware. This is done as follos.
var configuration = new OcelotMiddlewareConfiguration ```csharp
{ var configuration = new OcelotMiddlewareConfiguration
{
PreErrorResponderMiddleware = async (ctx, next) => PreErrorResponderMiddleware = async (ctx, next) =>
{ {
await next.Invoke(); 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. 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 allows a user to supply any behaviours they want before and after the Ocelot pipeline has run.
@ -363,6 +506,20 @@ http request before it is passed to Ocelots request creator.
Obviously you can just add middleware as normal before the call to app.UseOcelot() It cannot be added 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. 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 ## Not supported
Ocelot does not support... Ocelot does not support...
@ -381,8 +538,11 @@ forwarded to the downstream service. Obviously this would break everything :(
and doesnt check the response is OK. I think the fact you can even call stuff 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. that isnt available is annoying. Let alone it be null.
[![](https://codescene.io/projects/697/status.svg) Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results)
## Coming up ## Coming up
You can see what we are working on [here](https://github.com/TomPallister/Ocelot/projects/1) You can see what we are working on [here](https://github.com/TomPallister/Ocelot/projects/1)

564
README.md.orig Normal file
View File

@ -0,0 +1,564 @@
# Ocelot
[![Build status](https://ci.appveyor.com/api/projects/status/r6sv51qx36sis1je?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb)
[![Join the chat at https://gitter.im/Ocelotey/Lobby](https://badges.gitter.im/Ocelotey/Lobby.svg)](https://gitter.im/Ocelotey/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![](https://codescene.io/projects/697/status.svg) Get more details at **codescene.io**.](https://codescene.io/projects/697/jobs/latest-successful/results)
Attempt at a .NET Api Gateway
This project is aimed at people using .NET running
a micro services / service orientated architecture
that need a unified point of entry into their system.
In particular I want easy integration with
IdentityServer reference and bearer tokens.
We have been unable to find this in my current workplace
without having to write our own Javascript middlewares
to handle the IdentityServer reference tokens. We would
rather use the IdentityServer code that already exists
to do this.
Ocelot is a bunch of middlewares in a specific order.
Ocelot manipulates the HttpRequest object into a state specified by its configuration until
it reaches a request builder middleware where it creates a HttpRequestMessage object which is
used to make a request to a downstream service. The middleware that makes the request is
the last thing in the Ocelot pipeline. It does not call the next middleware.
The response from the downstream service is stored in a per request scoped repository
and retrived as the requests goes back up the Ocelot pipeline. There is a piece of middleware
that maps the HttpResponseMessage onto the HttpResponse object and that is returned to the client.
That is basically it with a bunch of other features.
## Contributing
Pull requests, issues and commentary welcome! No special process just create a request and get in
touch either via gitter or create an issue.
## How to install
Ocelot is designed to work with ASP.NET core only and is currently
built to netcoreapp1.1 [this](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) documentation may prove helpful when working out if Ocelot would be suitable for you.
Install Ocelot and it's dependecies using nuget. At the moment
all we have is the pre version. Once we have something working in
a half decent way we will drop a version.
`Install-Package Ocelot`
All versions can be found [here](https://www.nuget.org/packages/Ocelot/)
## Configuration
An example configuration can be found [here](https://github.com/TomPallister/Ocelot/blob/develop/test/Ocelot.ManualTest/configuration.json)
and an explained configuration can be found [here](https://github.com/TomPallister/Ocelot/blob/develop/configuration-explanation.txt). More detailed instructions to come on how to configure this.
There are two sections to the configuration. An array of ReRoutes and a GlobalConfiguration.
The ReRoutes are the objects that tell Ocelot how to treat an upstream request. The Global
configuration is a bit hacky and allows overrides of ReRoute specific settings. It's useful
if you don't want to manage lots of ReRoute specific settings.
```json
{
"ReRoutes": [],
"GlobalConfiguration": {}
}
```
More information on how to use these options is below..
## Startup
An example startup using a json file for configuration can be seen below.
Currently this is the only way to get configuration into Ocelot.
<<<<<<< HEAD
public class Startup
=======
```csharp
public class Startup
{
public Startup(IHostingEnvironment env)
>>>>>>> develop
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddJsonFile("configuration.json")
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
Action<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)

1
ReleaseNotes.md Normal file
View File

@ -0,0 +1 @@
No issues closed since last release

View File

@ -1,12 +0,0 @@
version: 1.0.{build}
configuration:
- Release
platform: Any CPU
build_script:
- build.bat
test_script:
- run-tests.bat
after_test:
- push-to-nuget.bat %appveyor_build_version%-rc1 %nugetApiKey%
cache:
- '%USERPROFILE%\.nuget\packages'

View File

@ -0,0 +1 @@
./build.ps1 -target BuildAndReleaseUnstable

View File

@ -1,2 +0,0 @@
./run-tests.bat
./build.bat

1
build-and-run-tests.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target RunTests

View File

@ -1,8 +0,0 @@
echo -------------------------
echo Building Ocelot
dotnet restore src/Ocelot
dotnet build src/Ocelot -c Release

388
build.cake Normal file
View File

@ -0,0 +1,388 @@
#tool "nuget:?package=GitVersion.CommandLine"
#tool "nuget:?package=OpenCover"
#tool "nuget:?package=ReportGenerator"
#tool "nuget:?package=GitReleaseNotes"
#addin "nuget:?package=Cake.DoInDirectory"
#addin "nuget:?package=Cake.Json"
// compile
var compileConfig = Argument("configuration", "Release");
var projectJson = "./src/Ocelot/project.json";
// build artifacts
var artifactsDir = Directory("artifacts");
// unit testing
var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests");
var unitTestAssemblies = @"./test/Ocelot.UnitTests";
// acceptance testing
var artifactsForAcceptanceTestsDir = artifactsDir + Directory("AcceptanceTests");
// integration testing
var artifactsForIntegrationTestsDir = artifactsDir + Directory("IntegrationTests");
// benchmark testing
var artifactsForBenchmarkTestsDir = artifactsDir + Directory("BenchmarkTests");
var benchmarkTestAssemblies = @"./test/Ocelot.Benchmarks";
// packaging
var packagesDir = artifactsDir + Directory("Packages");
var releaseNotesFile = packagesDir + File("releasenotes.md");
var artifactsFile = packagesDir + File("artifacts.txt");
// unstable releases
var nugetFeedUnstableKey = EnvironmentVariable("nuget-apikey-unstable");
var nugetFeedUnstableUploadUrl = "https://www.nuget.org/api/v2/package";
var nugetFeedUnstableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package";
// stable releases
var tagsUrl = "https://api.github.com/repos/tompallister/ocelot/releases/tags/";
var nugetFeedStableKey = EnvironmentVariable("nuget-apikey-stable");
var nugetFeedStableUploadUrl = "https://www.nuget.org/api/v2/package";
var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package";
// internal build variables - don't change these.
var releaseTag = "";
string committedVersion = "0.0.0-dev";
var buildVersion = committedVersion;
GitVersion versioning = null;
var nugetFeedUnstableBranchFilter = "^(develop)$|^(PullRequest/)";
var target = Argument("target", "Default");
Information("target is " +target);
Information("Build configuration is " + compileConfig);
Task("Default")
.IsDependentOn("Build");
Task("Build")
.IsDependentOn("RunTests")
.IsDependentOn("CreatePackages");
Task("BuildAndReleaseUnstable")
.IsDependentOn("Build")
.IsDependentOn("ReleasePackagesToUnstableFeed");
Task("Clean")
.Does(() =>
{
if (DirectoryExists(artifactsDir))
{
DeleteDirectory(artifactsDir, recursive:true);
}
CreateDirectory(artifactsDir);
});
Task("Version")
.Does(() =>
{
versioning = GetNuGetVersionForCommit();
var nugetVersion = versioning.NuGetVersion;
Information("SemVer version number: " + nugetVersion);
if (AppVeyor.IsRunningOnAppVeyor)
{
Information("Persisting version number...");
PersistVersion(committedVersion, nugetVersion);
buildVersion = nugetVersion;
}
else
{
Information("We are not running on build server, so we won't persist the version number.");
}
});
Task("Restore")
.IsDependentOn("Clean")
.IsDependentOn("Version")
.Does(() =>
{
DotNetCoreRestore("./src");
DotNetCoreRestore("./test");
});
Task("RunUnitTests")
.IsDependentOn("Restore")
.Does(() =>
{
var buildSettings = new DotNetCoreTestSettings
{
Configuration = compileConfig,
};
EnsureDirectoryExists(artifactsForUnitTestsDir);
DotNetCoreTest(unitTestAssemblies, buildSettings);
});
Task("RunAcceptanceTests")
.IsDependentOn("Restore")
.Does(() =>
{
var buildSettings = new DotNetCoreTestSettings
{
Configuration = "Debug", //acceptance test config is hard-coded for debug
};
EnsureDirectoryExists(artifactsForAcceptanceTestsDir);
DoInDirectory("test/Ocelot.AcceptanceTests", () =>
{
DotNetCoreTest(".", buildSettings);
});
});
Task("RunIntegrationTests")
.IsDependentOn("Restore")
.Does(() =>
{
var buildSettings = new DotNetCoreTestSettings
{
Configuration = "Debug", //int test config is hard-coded for debug
};
EnsureDirectoryExists(artifactsForIntegrationTestsDir);
DoInDirectory("test/Ocelot.IntegrationTests", () =>
{
DotNetCoreTest(".", buildSettings);
});
});
Task("RunBenchmarkTests")
.IsDependentOn("Restore")
.Does(() =>
{
var buildSettings = new DotNetCoreRunSettings
{
Configuration = compileConfig,
};
EnsureDirectoryExists(artifactsForBenchmarkTestsDir);
DoInDirectory(benchmarkTestAssemblies, () =>
{
DotNetCoreRun(".", "", buildSettings);
});
});
Task("RunTests")
.IsDependentOn("RunUnitTests")
.IsDependentOn("RunAcceptanceTests")
.IsDependentOn("RunIntegrationTests")
.Does(() =>
{
});
Task("CreatePackages")
.Does(() =>
{
EnsureDirectoryExists(packagesDir);
GenerateReleaseNotes(releaseNotesFile);
var settings = new DotNetCorePackSettings
{
OutputDirectory = packagesDir,
NoBuild = true
};
DotNetCorePack(projectJson, settings);
System.IO.File.WriteAllLines(artifactsFile, new[]{
"nuget:Ocelot." + buildVersion + ".nupkg",
"nugetSymbols:Ocelot." + buildVersion + ".symbols.nupkg",
"releaseNotes:releasenotes.md"
});
if (AppVeyor.IsRunningOnAppVeyor)
{
var path = packagesDir.ToString() + @"/**/*";
foreach (var file in GetFiles(path))
{
AppVeyor.UploadArtifact(file.FullPath);
}
}
});
Task("ReleasePackagesToUnstableFeed")
.IsDependentOn("CreatePackages")
.Does(() =>
{
if (ShouldPublishToUnstableFeed(nugetFeedUnstableBranchFilter, versioning.BranchName))
{
PublishPackages(packagesDir, artifactsFile, nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl);
}
});
Task("EnsureStableReleaseRequirements")
.Does(() =>
{
if (!AppVeyor.IsRunningOnAppVeyor)
{
throw new Exception("Stable release should happen via appveyor");
}
var isTag =
AppVeyor.Environment.Repository.Tag.IsTag &&
!string.IsNullOrWhiteSpace(AppVeyor.Environment.Repository.Tag.Name);
if (!isTag)
{
throw new Exception("Stable release should happen from a published GitHub release");
}
});
Task("UpdateVersionInfo")
.IsDependentOn("EnsureStableReleaseRequirements")
.Does(() =>
{
releaseTag = AppVeyor.Environment.Repository.Tag.Name;
AppVeyor.UpdateBuildVersion(releaseTag);
});
Task("DownloadGitHubReleaseArtifacts")
.IsDependentOn("UpdateVersionInfo")
.Does(() =>
{
EnsureDirectoryExists(packagesDir);
var releaseUrl = tagsUrl + releaseTag;
var assets_url = ParseJson(GetResource(releaseUrl))
.GetValue("assets_url")
.Value<string>();
foreach(var asset in DeserializeJson<JArray>(GetResource(assets_url)))
{
var file = packagesDir + File(asset.Value<string>("name"));
Information("Downloading " + file);
DownloadFile(asset.Value<string>("browser_download_url"), file);
}
});
Task("ReleasePackagesToStableFeed")
.IsDependentOn("DownloadGitHubReleaseArtifacts")
.Does(() =>
{
PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl);
});
Task("Release")
.IsDependentOn("ReleasePackagesToStableFeed");
RunTarget(target);
/// Gets nuique nuget version for this commit
private GitVersion GetNuGetVersionForCommit()
{
GitVersion(new GitVersionSettings{
UpdateAssemblyInfo = false,
OutputType = GitVersionOutput.BuildServer
});
return GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json });
}
/// Updates project version in all of our projects
private void PersistVersion(string committedVersion, string newVersion)
{
Information(string.Format("We'll search all project.json files for {0} and replace with {1}...", committedVersion, newVersion));
var projectJsonFiles = GetFiles("./**/project.json");
foreach(var projectJsonFile in projectJsonFiles)
{
var file = projectJsonFile.ToString();
Information(string.Format("Updating {0}...", file));
var updatedProjectJson = System.IO.File.ReadAllText(file)
.Replace(committedVersion, newVersion);
System.IO.File.WriteAllText(file, updatedProjectJson);
}
}
/// generates release notes based on issues closed in GitHub since the last release
private void GenerateReleaseNotes(ConvertableFilePath file)
{
if (!IsRunningOnWindows())
{
Warning("We can't generate release notes as we're not running on Windows.");
return;
}
Information("Generating release notes at " + file);
var releaseNotesExitCode = StartProcess(
@"tools/GitReleaseNotes/tools/gitreleasenotes.exe",
new ProcessSettings { Arguments = ". /o " + file });
if (string.IsNullOrEmpty(System.IO.File.ReadAllText(file)))
{
System.IO.File.WriteAllText(file, "No issues closed since last release");
}
if (releaseNotesExitCode != 0)
{
throw new Exception("Failed to generate release notes");
}
}
/// Publishes code and symbols packages to nuget feed, based on contents of artifacts file
private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFilePath artifactsFile, string feedApiKey, string codeFeedUrl, string symbolFeedUrl)
{
var artifacts = System.IO.File
.ReadAllLines(artifactsFile)
.Select(l => l.Split(':'))
.ToDictionary(v => v[0], v => v[1]);
var codePackage = packagesDir + File(artifacts["nuget"]);
Information("Pushing package " + codePackage);
NuGetPush(
codePackage,
new NuGetPushSettings {
ApiKey = feedApiKey,
Source = codeFeedUrl
});
}
/// gets the resource from the specified url
private string GetResource(string url)
{
Information("Getting resource from " + url);
var assetsRequest = System.Net.WebRequest.CreateHttp(url);
assetsRequest.Method = "GET";
assetsRequest.Accept = "application/vnd.github.v3+json";
assetsRequest.UserAgent = "BuildScript";
using (var assetsResponse = assetsRequest.GetResponse())
{
var assetsStream = assetsResponse.GetResponseStream();
var assetsReader = new StreamReader(assetsStream);
return assetsReader.ReadToEnd();
}
}
private bool ShouldPublishToUnstableFeed(string filter, string branchName)
{
var regex = new System.Text.RegularExpressions.Regex(filter);
var publish = regex.IsMatch(branchName);
if (publish)
{
Information("Branch " + branchName + " will be published to the unstable feed");
}
else
{
Information("Branch " + branchName + " will not be published to the unstable feed");
}
return publish;
}

189
build.ps1 Normal file
View File

@ -0,0 +1,189 @@
##########################################################################
# This is the Cake bootstrapper script for PowerShell.
# This file was downloaded from https://github.com/cake-build/resources
# Feel free to change this file to fit your needs.
##########################################################################
<#
.SYNOPSIS
This is a Powershell script to bootstrap a Cake build.
.DESCRIPTION
This Powershell script will download NuGet if missing, restore NuGet tools (including Cake)
and execute your Cake build script with the parameters you provide.
.PARAMETER Script
The build script to execute.
.PARAMETER Target
The build script target to run.
.PARAMETER Configuration
The build configuration to use.
.PARAMETER Verbosity
Specifies the amount of information to be displayed.
.PARAMETER Experimental
Tells Cake to use the latest Roslyn release.
.PARAMETER WhatIf
Performs a dry run of the build script.
No tasks will be executed.
.PARAMETER Mono
Tells Cake to use the Mono scripting engine.
.PARAMETER SkipToolPackageRestore
Skips restoring of packages.
.PARAMETER ScriptArgs
Remaining arguments are added here.
.LINK
http://cakebuild.net
#>
[CmdletBinding()]
Param(
[string]$Script = "build.cake",
[string]$Target = "Default",
[ValidateSet("Release", "Debug")]
[string]$Configuration = "Release",
[ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
[string]$Verbosity = "Verbose",
[switch]$Experimental,
[Alias("DryRun","Noop")]
[switch]$WhatIf,
[switch]$Mono,
[switch]$SkipToolPackageRestore,
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$ScriptArgs
)
[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null
function MD5HashFile([string] $filePath)
{
if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf))
{
return $null
}
[System.IO.Stream] $file = $null;
[System.Security.Cryptography.MD5] $md5 = $null;
try
{
$md5 = [System.Security.Cryptography.MD5]::Create()
$file = [System.IO.File]::OpenRead($filePath)
return [System.BitConverter]::ToString($md5.ComputeHash($file))
}
finally
{
if ($file -ne $null)
{
$file.Dispose()
}
}
}
Write-Host "Preparing to run build script..."
if(!$PSScriptRoot){
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
}
$TOOLS_DIR = Join-Path $PSScriptRoot "tools"
$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe"
$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe"
$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config"
$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum"
# Should we use mono?
$UseMono = "";
if($Mono.IsPresent) {
Write-Verbose -Message "Using the Mono based scripting engine."
$UseMono = "-mono"
}
# Should we use the new Roslyn?
$UseExperimental = "";
if($Experimental.IsPresent -and !($Mono.IsPresent)) {
Write-Verbose -Message "Using experimental version of Roslyn."
$UseExperimental = "-experimental"
}
# Is this a dry run?
$UseDryRun = "";
if($WhatIf.IsPresent) {
$UseDryRun = "-dryrun"
}
# Make sure tools folder exists
if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) {
Write-Verbose -Message "Creating tools directory..."
New-Item -Path $TOOLS_DIR -Type directory | out-null
}
# Make sure that packages.config exist.
if (!(Test-Path $PACKAGES_CONFIG)) {
Write-Verbose -Message "Downloading packages.config..."
try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch {
Throw "Could not download packages.config."
}
}
# Try find NuGet.exe in path if not exists
if (!(Test-Path $NUGET_EXE)) {
Write-Verbose -Message "Trying to find nuget.exe in PATH..."
$existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) }
$NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1
if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) {
Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)."
$NUGET_EXE = $NUGET_EXE_IN_PATH.FullName
}
}
# Try download NuGet.exe if not exists
if (!(Test-Path $NUGET_EXE)) {
Write-Verbose -Message "Downloading NuGet.exe..."
try {
(New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE)
} catch {
Throw "Could not download NuGet.exe."
}
}
# Save nuget.exe path to environment to be available to child processed
$ENV:NUGET_EXE = $NUGET_EXE
# Restore tools from NuGet?
if(-Not $SkipToolPackageRestore.IsPresent) {
Push-Location
Set-Location $TOOLS_DIR
# Check for changes in packages.config and remove installed tools if true.
[string] $md5Hash = MD5HashFile($PACKAGES_CONFIG)
if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or
($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) {
Write-Verbose -Message "Missing or changed package.config hash..."
Remove-Item * -Recurse -Exclude packages.config,nuget.exe
}
Write-Verbose -Message "Restoring tools from NuGet..."
$NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`""
if ($LASTEXITCODE -ne 0) {
Throw "An error occured while restoring NuGet tools."
}
else
{
$md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII"
}
Write-Verbose -Message ($NuGetOutput | out-string)
Pop-Location
}
# Make sure that Cake has been installed.
if (!(Test-Path $CAKE_EXE)) {
Throw "Could not find Cake.exe at $CAKE_EXE"
}
# Start Cake
Write-Host "Running build script..."
Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs"
exit $LASTEXITCODE

22
build.readme.md Normal file
View File

@ -0,0 +1,22 @@
#1. Overview
This document summarises the build and release process for the project. The build scripts are written using [Cake](http://cakebuild.net/), and are defined in `./build.cake`. The scripts have been designed to be run by either developers locally or by a build server (currently [AppVeyor](https://www.appveyor.com/)), with minimal logic defined in the build server itself.
#2. Building
* You'll generally want to run the `./build.ps1` script. This will compile, run unit and acceptance tests and build the output packages locally. Output will got to the `./artifacts` directory.
* You can view the current commit's [SemVer](http://semver.org/) build information by running `./version.ps1`.
* The other `./*.ps1` scripts perform subsets of the build process, if you don't want to run the full build.
* The release process works best with GitFlow branching; this allows us to publish every development commit to an unstable feed with a unique SemVer version, and then choose when to release to a stable feed.
#3. Release process
This section defines the release process for the maintainers of the project.
* Merge pull requests to the `release` branch.
* Every commit pushed to the Origin repo will kick off the [ocelot-build](https://ci.appveyor.com/project/binarymash/ocelot) project in AppVeyor. This performs the same tasks as the command line build, and in addition pushes the packages to the unstable nuget feed.
* When you're ready for a release, create a release branch. You'll probably want to update the committed `./ReleaseNotes.md` based on the contents of the equivalent file in the `./artifacts` directory.
* When the `release` branch has built successfully in Appveyor, select the build and then Deploy to the `GitHub Release` environment. This will create a new release in GitHub.
* In Github, navigate to the [release](https://github.com/binarymash/Ocelot/releases). Modify the release name and tag as desired.
* When you're ready, publish the release. This will tag the commit with the specified release number.
* The [ocelot-release](https://ci.appveyor.com/project/binarymash/ocelot-wtaj9) project will detect the newly created tag and kick off the release process. This will download the artifacts from GitHub, and publish the packages to the stable nuget feed.
* When you have a final stable release build, merge the `release` branch into `master` and `develop`. Deploy the master branch to github and following the full release process as described above. Don't forget to uncheck the "This is a pre-release" checkbox in GitHub before publishing.
* Note - because the release builds are initiated by tagging a commit, if for some reason a release build fails in AppVeyor you'll need to delete the tag from the repo and republish the release in GitHub.

101
build.sh Executable file
View File

@ -0,0 +1,101 @@
#!/usr/bin/env bash
##########################################################################
# This is the Cake bootstrapper script for Linux and OS X.
# This file was downloaded from https://github.com/cake-build/resources
# Feel free to change this file to fit your needs.
##########################################################################
# Define directories.
SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
TOOLS_DIR=$SCRIPT_DIR/tools
NUGET_EXE=$TOOLS_DIR/nuget.exe
CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe
PACKAGES_CONFIG=$TOOLS_DIR/packages.config
PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum
# Define md5sum or md5 depending on Linux/OSX
MD5_EXE=
if [[ "$(uname -s)" == "Darwin" ]]; then
MD5_EXE="md5 -r"
else
MD5_EXE="md5sum"
fi
# Define default arguments.
SCRIPT="build.cake"
TARGET="Default"
CONFIGURATION="Release"
VERBOSITY="verbose"
DRYRUN=
SHOW_VERSION=false
SCRIPT_ARGUMENTS=()
# Parse arguments.
for i in "$@"; do
case $1 in
-s|--script) SCRIPT="$2"; shift ;;
-t|--target) TARGET="$2"; shift ;;
-c|--configuration) CONFIGURATION="$2"; shift ;;
-v|--verbosity) VERBOSITY="$2"; shift ;;
-d|--dryrun) DRYRUN="-dryrun" ;;
--version) SHOW_VERSION=true ;;
--) shift; SCRIPT_ARGUMENTS+=("$@"); break ;;
*) SCRIPT_ARGUMENTS+=("$1") ;;
esac
shift
done
# Make sure the tools folder exist.
if [ ! -d "$TOOLS_DIR" ]; then
mkdir "$TOOLS_DIR"
fi
# Make sure that packages.config exist.
if [ ! -f "$TOOLS_DIR/packages.config" ]; then
echo "Downloading packages.config..."
curl -Lsfo "$TOOLS_DIR/packages.config" http://cakebuild.net/download/bootstrapper/packages
if [ $? -ne 0 ]; then
echo "An error occured while downloading packages.config."
exit 1
fi
fi
# Download NuGet if it does not exist.
if [ ! -f "$NUGET_EXE" ]; then
echo "Downloading NuGet..."
curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe
if [ $? -ne 0 ]; then
echo "An error occured while downloading nuget.exe."
exit 1
fi
fi
# Restore tools from NuGet.
pushd "$TOOLS_DIR" >/dev/null
if [ ! -f "$PACKAGES_CONFIG_MD5" ] || [ "$( cat "$PACKAGES_CONFIG_MD5" | sed 's/\r$//' )" != "$( $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' )" ]; then
find . -type d ! -name . | xargs rm -rf
fi
mono "$NUGET_EXE" install -ExcludeVersion
if [ $? -ne 0 ]; then
echo "Could not restore NuGet packages."
exit 1
fi
$MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' >| "$PACKAGES_CONFIG_MD5"
popd >/dev/null
# Make sure that Cake has been installed.
if [ ! -f "$CAKE_EXE" ]; then
echo "Could not find Cake.exe at '$CAKE_EXE'."
exit 1
fi
# Start Cake
if $SHOW_VERSION; then
exec mono "$CAKE_EXE" -version
else
exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY -configuration=$CONFIGURATION -target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}"
fi

View File

@ -1,12 +1,21 @@
{ {
"ReRoutes": [ "ReRoutes": [
{ {
# The url we are forwarding the request to, ocelot will not add a trailing slash # The downstream path we are forwarding the request to, ocelot will not add a trailing slash.
"DownstreamTemplate": "http://somehost.com/identityserverexample", # Ocelot replaces any placeholders {etc} with matched values from the incoming request.
# The path we are listening on for this re route, Ocelot will add a trailing slash to "DownstreamPathTemplate": "/identityserverexample/{someid}/something",
# this property. Then when a request is made Ocelot makes sure a trailing slash is added # The scheme you want Ocelot to use when making the downstream request
# to that so everything matches "DownstreamScheme": "https",
"UpstreamTemplate": "/identityserverexample", # The port you want Ocelot to use when making the downstream request, will default to
# scheme if nothing set
"DownstreamPort": 80,
# The host address of the downstream service, should not have a trailing slash or scheme
# if there is a trailing slash Ocelot will remove it.
"DownstreamHost" "localhost"
# The path template we are listening on for this re route, Ocelot will add a trailing
# slash to this property. Then when a request is made Ocelot makes sure a trailing
# slash is added, so everything matches
"UpstreamPathTemplate": "/identityserverexample",
# The method we are listening for on this re route # The method we are listening for on this re route
"UpstreamHttpMethod": "Get", "UpstreamHttpMethod": "Get",
# Only support identity server at the moment # Only support identity server at the moment
@ -71,12 +80,24 @@
# the caching a lot. # the caching a lot.
"FileCacheOptions": { "TtlSeconds": 15 }, "FileCacheOptions": { "TtlSeconds": 15 },
# The value of this is used when matching the upstream template to an upstream url. # The value of this is used when matching the upstream template to an upstream url.
"ReRouteIsCaseSensitive": false "ReRouteIsCaseSensitive": false,
# Tells Ocelot the name of the service it is looking when making requests to service discovery
# for hosts and ports
"ServiceName": "product"
# Tells Ocelot which load balancer to use when making downstream requests.
"LoadBalancer": "RoundRobin"
}, },
# This section is meant to be for global configuration settings # This section is meant to be for global configuration settings
"GlobalConfiguration": { "GlobalConfiguration": {
# If this is set it will override any route specific request id keys, behaves the same # If this is set it will override any route specific request id keys, behaves the same
# otherwise # otherwise
"RequestIdKey": "OcRequestId", "RequestIdKey": "OcRequestId",
# If set Ocelot will try and use service discovery to locate downstream hosts and ports
"ServiceDiscoveryProvider":
{
"Provider":"Consul",
"Host":"localhost",
"Port":8500
}
} }
} }

1
configuration.json Executable file
View File

@ -0,0 +1 @@
{"ReRoutes":[],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Host":null,"Port":0},"AdministrationPath":"/administration"}}

3
configuration.yaml Executable file
View File

@ -0,0 +1,3 @@
Routes:
- Downstream: http://localhost:51879/
Upstream: /heee

File diff suppressed because one or more lines are too long

View File

@ -1,11 +0,0 @@
echo -------------------------
echo Packing Ocelot Version %1
nuget pack .\Ocelot.nuspec -version %1
echo Publishing Ocelot
nuget push Ocelot.%1.nupkg -ApiKey %2 -Source https://www.nuget.org/api/v2/package

1
release.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target Release

1
run-acceptance-tests.ps1 Normal file
View File

@ -0,0 +1 @@
./build -target RunAcceptanceTests

View File

@ -1,15 +0,0 @@
echo -------------------------
echo Running Ocelot.Benchmarks
cd test/Ocelot.Benchmarks
dotnet restore
dotnet run
cd ../../

1
run-benchmarks.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target RunBenchmarkTests

View File

@ -1,17 +0,0 @@
echo -------------------------
echo Restoring Ocelot
dotnet restore src/Ocelot
echo Restoring Ocelot.ManualTest
dotnet restore test/Ocelot.ManualTest/
echo Running Ocelot.UnitTests
dotnet restore test/Ocelot.UnitTests/
dotnet test test/Ocelot.UnitTests/
echo Running Ocelot.AcceptanceTests
cd test/Ocelot.AcceptanceTests/
dotnet restore
dotnet test
cd ../../

1
run-unit-tests.ps1 Normal file
View File

@ -0,0 +1 @@
./build.ps1 -target RunUnitTests

View File

@ -12,18 +12,18 @@ namespace Ocelot.Authentication.Handler.Creator
/// </summary> /// </summary>
public class AuthenticationHandlerCreator : IAuthenticationHandlerCreator public class AuthenticationHandlerCreator : IAuthenticationHandlerCreator
{ {
public Response<RequestDelegate> CreateIdentityServerAuthenticationHandler(IApplicationBuilder app, AuthenticationOptions authOptions) public Response<RequestDelegate> Create(IApplicationBuilder app, AuthenticationOptions authOptions)
{ {
var builder = app.New(); var builder = app.New();
builder.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions builder.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions
{ {
Authority = authOptions.ProviderRootUrl, Authority = authOptions.ProviderRootUrl,
ScopeName = authOptions.ScopeName, ApiName = authOptions.ScopeName,
RequireHttpsMetadata = authOptions.RequireHttps, RequireHttpsMetadata = authOptions.RequireHttps,
AdditionalScopes = authOptions.AdditionalScopes, AllowedScopes = authOptions.AdditionalScopes,
SupportedTokens = SupportedTokens.Both, SupportedTokens = SupportedTokens.Both,
ScopeSecret = authOptions.ScopeSecret ApiSecret = authOptions.ScopeSecret
}); });
var authenticationNext = builder.Build(); var authenticationNext = builder.Build();

View File

@ -8,6 +8,6 @@ namespace Ocelot.Authentication.Handler.Creator
public interface IAuthenticationHandlerCreator public interface IAuthenticationHandlerCreator
{ {
Response<RequestDelegate> CreateIdentityServerAuthenticationHandler(IApplicationBuilder app, AuthenticationOptions authOptions); Response<RequestDelegate> Create(IApplicationBuilder app, AuthenticationOptions authOptions);
} }
} }

View File

@ -19,7 +19,7 @@ namespace Ocelot.Authentication.Handler.Factory
public Response<AuthenticationHandler> Get(IApplicationBuilder app, AuthenticationOptions authOptions) public Response<AuthenticationHandler> Get(IApplicationBuilder app, AuthenticationOptions authOptions)
{ {
var handler = _creator.CreateIdentityServerAuthenticationHandler(app, authOptions); var handler = _creator.Create(app, authOptions);
if (!handler.IsError) if (!handler.IsError)
{ {

View File

@ -2,7 +2,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Ocelot.Authentication.Handler.Factory; using Ocelot.Authentication.Handler.Factory;
using Ocelot.Configuration; using Ocelot.Configuration;
using Ocelot.Errors; using Ocelot.Errors;

View File

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using Ocelot.Errors; using Ocelot.Errors;
using Ocelot.Responses; using Ocelot.Responses;

View File

@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.Responses; using Ocelot.Responses;
@ -61,7 +60,7 @@ namespace Ocelot.Authorisation.Middleware
SetPipelineError(new List<Error> SetPipelineError(new List<Error>
{ {
new UnauthorisedError( new UnauthorisedError(
$"{context.User.Identity.Name} unable to access {DownstreamRoute.ReRoute.UpstreamTemplate}") $"{context.User.Identity.Name} unable to access {DownstreamRoute.ReRoute.UpstreamPathTemplate.Value}")
}); });
} }
} }

View File

@ -2,7 +2,6 @@
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.Middleware; using Ocelot.Middleware;

View File

@ -1,7 +1,6 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.Middleware; using Ocelot.Middleware;

View File

@ -0,0 +1,22 @@
using System;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace Ocelot.Configuration.Authentication
{
public class HashMatcher : IHashMatcher
{
public bool Match(string password, string salt, string hash)
{
byte[] s = Convert.FromBase64String(salt);
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: password,
salt: s,
prf: KeyDerivationPrf.HMACSHA256,
iterationCount: 10000,
numBytesRequested: 256 / 8));
return hashed == hash;
}
}
}

View File

@ -0,0 +1,7 @@
namespace Ocelot.Configuration.Authentication
{
public interface IHashMatcher
{
bool Match(string password, string salt, string hash);
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using IdentityServer4.Models;
using IdentityServer4.Validation;
using Ocelot.Configuration.Provider;
namespace Ocelot.Configuration.Authentication
{
public class OcelotResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private readonly IHashMatcher _matcher;
private readonly IIdentityServerConfiguration _identityServerConfiguration;
public OcelotResourceOwnerPasswordValidator(IHashMatcher matcher, IIdentityServerConfiguration identityServerConfiguration)
{
_identityServerConfiguration = identityServerConfiguration;
_matcher = matcher;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
try
{
var user = _identityServerConfiguration.Users.FirstOrDefault(u => u.UserName == context.UserName);
if(user == null)
{
context.Result = new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
"invalid custom credential");
}
else if(_matcher.Match(context.Password, user.Salt, user.Hash))
{
context.Result = new GrantValidationResult(
subject: "admin",
authenticationMethod: "custom");
}
else
{
context.Result = new GrantValidationResult(
TokenRequestErrors.InvalidGrant,
"invalid custom credential");
}
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
}
}
}

View File

@ -0,0 +1,56 @@
using System.Collections.Generic;
namespace Ocelot.Configuration.Builder
{
public class AuthenticationOptionsBuilder
{
private string _provider;
private string _providerRootUrl;
private string _scopeName;
private string _scopeSecret;
private bool _requireHttps;
private List<string> _additionalScopes;
public AuthenticationOptionsBuilder WithProvider(string provider)
{
_provider = provider;
return this;
}
public AuthenticationOptionsBuilder WithProviderRootUrl(string providerRootUrl)
{
_providerRootUrl = providerRootUrl;
return this;
}
public AuthenticationOptionsBuilder WithScopeName(string scopeName)
{
_scopeName = scopeName;
return this;
}
public AuthenticationOptionsBuilder WithScopeSecret(string scopeSecret)
{
_scopeSecret = scopeSecret;
return this;
}
public AuthenticationOptionsBuilder WithRequireHttps(bool requireHttps)
{
_requireHttps = requireHttps;
return this;
}
public AuthenticationOptionsBuilder WithAdditionalScopes(List<string> additionalScopes)
{
_additionalScopes = additionalScopes;
return this;
}
public AuthenticationOptions Build()
{
return new AuthenticationOptions(_provider, _providerRootUrl, _scopeName, _requireHttps, _additionalScopes, _scopeSecret);
}
}
}

View File

@ -0,0 +1,34 @@
namespace Ocelot.Configuration.Builder
{
public class QoSOptionsBuilder
{
private int _exceptionsAllowedBeforeBreaking;
private int _durationOfBreak;
private int _timeoutValue;
public QoSOptionsBuilder WithExceptionsAllowedBeforeBreaking(int exceptionsAllowedBeforeBreaking)
{
_exceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking;
return this;
}
public QoSOptionsBuilder WithDurationOfBreak(int durationOfBreak)
{
_durationOfBreak = durationOfBreak;
return this;
}
public QoSOptionsBuilder WithTimeoutValue(int timeoutValue)
{
_timeoutValue = timeoutValue;
return this;
}
public QoSOptions Build()
{
return new QoSOptions(_exceptionsAllowedBeforeBreaking, _durationOfBreak, _timeoutValue);
}
}
}

View File

@ -1,20 +1,18 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http;
using Ocelot.Values;
namespace Ocelot.Configuration.Builder namespace Ocelot.Configuration.Builder
{ {
public class ReRouteBuilder public class ReRouteBuilder
{ {
private string _downstreamTemplate; private AuthenticationOptions _authenticationOptions;
private string _loadBalancerKey;
private string _downstreamPathTemplate;
private string _upstreamTemplate; private string _upstreamTemplate;
private string _upstreamTemplatePattern; private string _upstreamTemplatePattern;
private string _upstreamHttpMethod; private string _upstreamHttpMethod;
private bool _isAuthenticated; private bool _isAuthenticated;
private string _authenticationProvider;
private string _authenticationProviderUrl;
private string _scopeName;
private List<string> _additionalScopes;
private bool _requireHttps;
private string _scopeSecret;
private List<ClaimToThing> _configHeaderExtractorProperties; private List<ClaimToThing> _configHeaderExtractorProperties;
private List<ClaimToThing> _claimToClaims; private List<ClaimToThing> _claimToClaims;
private Dictionary<string, string> _routeClaimRequirement; private Dictionary<string, string> _routeClaimRequirement;
@ -23,19 +21,41 @@ namespace Ocelot.Configuration.Builder
private string _requestIdHeaderKey; private string _requestIdHeaderKey;
private bool _isCached; private bool _isCached;
private CacheOptions _fileCacheOptions; private CacheOptions _fileCacheOptions;
private string _downstreamScheme;
private string _downstreamHost;
private int _downstreamPort;
private string _loadBalancer;
private ServiceProviderConfiguraion _serviceProviderConfiguraion;
private bool _useQos;
private QoSOptions _qosOptions;
public bool _enableRateLimiting;
public RateLimitOptions _rateLimitOptions;
public ReRouteBuilder() public ReRouteBuilder WithLoadBalancer(string loadBalancer)
{ {
_additionalScopes = new List<string>(); _loadBalancer = loadBalancer;
}
public ReRouteBuilder WithDownstreamTemplate(string input)
{
_downstreamTemplate = input;
return this; return this;
} }
public ReRouteBuilder WithUpstreamTemplate(string input) public ReRouteBuilder WithDownstreamScheme(string downstreamScheme)
{
_downstreamScheme = downstreamScheme;
return this;
}
public ReRouteBuilder WithDownstreamHost(string downstreamHost)
{
_downstreamHost = downstreamHost;
return this;
}
public ReRouteBuilder WithDownstreamPathTemplate(string input)
{
_downstreamPathTemplate = input;
return this;
}
public ReRouteBuilder WithUpstreamPathTemplate(string input)
{ {
_upstreamTemplate = input; _upstreamTemplate = input;
return this; return this;
@ -63,42 +83,6 @@ namespace Ocelot.Configuration.Builder
return this; return this;
} }
public ReRouteBuilder WithAuthenticationProvider(string input)
{
_authenticationProvider = input;
return this;
}
public ReRouteBuilder WithAuthenticationProviderUrl(string input)
{
_authenticationProviderUrl = input;
return this;
}
public ReRouteBuilder WithAuthenticationProviderScopeName(string input)
{
_scopeName = input;
return this;
}
public ReRouteBuilder WithAuthenticationProviderAdditionalScopes(List<string> input)
{
_additionalScopes = input;
return this;
}
public ReRouteBuilder WithRequireHttps(bool input)
{
_requireHttps = input;
return this;
}
public ReRouteBuilder WithScopeSecret(string input)
{
_scopeSecret = input;
return this;
}
public ReRouteBuilder WithRequestIdKey(string input) public ReRouteBuilder WithRequestIdKey(string input)
{ {
_requestIdHeaderKey = input; _requestIdHeaderKey = input;
@ -141,12 +125,83 @@ namespace Ocelot.Configuration.Builder
return this; return this;
} }
public ReRouteBuilder WithDownstreamPort(int port)
{
_downstreamPort = port;
return this;
}
public ReRouteBuilder WithIsQos(bool input)
{
_useQos = input;
return this;
}
public ReRouteBuilder WithQosOptions(QoSOptions input)
{
_qosOptions = input;
return this;
}
public ReRouteBuilder WithLoadBalancerKey(string loadBalancerKey)
{
_loadBalancerKey = loadBalancerKey;
return this;
}
public ReRouteBuilder WithServiceProviderConfiguraion(ServiceProviderConfiguraion serviceProviderConfiguraion)
{
_serviceProviderConfiguraion = serviceProviderConfiguraion;
return this;
}
public ReRouteBuilder WithAuthenticationOptions(AuthenticationOptions authenticationOptions)
{
_authenticationOptions = authenticationOptions;
return this;
}
public ReRouteBuilder WithEnableRateLimiting(bool input)
{
_enableRateLimiting = input;
return this;
}
public ReRouteBuilder WithRateLimitOptions(RateLimitOptions input)
{
_rateLimitOptions = input;
return this;
}
public ReRoute Build() public ReRoute Build()
{ {
return new ReRoute(_downstreamTemplate, _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, return new ReRoute(
_isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, new PathTemplate(_downstreamPathTemplate),
_requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement, new PathTemplate(_upstreamTemplate),
_isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions); new HttpMethod(_upstreamHttpMethod),
_upstreamTemplatePattern,
_isAuthenticated,
_authenticationOptions,
_configHeaderExtractorProperties,
_claimToClaims,
_routeClaimRequirement,
_isAuthorised,
_claimToQueries,
_requestIdHeaderKey,
_isCached,
_fileCacheOptions,
_downstreamScheme,
_loadBalancer,
_downstreamHost,
_downstreamPort,
_loadBalancerKey,
_serviceProviderConfiguraion,
_useQos,
_qosOptions,
_enableRateLimiting,
_rateLimitOptions);
} }
} }
} }

View File

@ -0,0 +1,62 @@
namespace Ocelot.Configuration.Builder
{
public class ServiceProviderConfiguraionBuilder
{
private string _serviceName;
private string _downstreamHost;
private int _downstreamPort;
private bool _userServiceDiscovery;
private string _serviceDiscoveryProvider;
private string _serviceDiscoveryProviderHost;
private int _serviceDiscoveryProviderPort;
public ServiceProviderConfiguraionBuilder WithServiceName(string serviceName)
{
_serviceName = serviceName;
return this;
}
public ServiceProviderConfiguraionBuilder WithDownstreamHost(string downstreamHost)
{
_downstreamHost = downstreamHost;
return this;
}
public ServiceProviderConfiguraionBuilder WithDownstreamPort(int downstreamPort)
{
_downstreamPort = downstreamPort;
return this;
}
public ServiceProviderConfiguraionBuilder WithUseServiceDiscovery(bool userServiceDiscovery)
{
_userServiceDiscovery = userServiceDiscovery;
return this;
}
public ServiceProviderConfiguraionBuilder WithServiceDiscoveryProvider(string serviceDiscoveryProvider)
{
_serviceDiscoveryProvider = serviceDiscoveryProvider;
return this;
}
public ServiceProviderConfiguraionBuilder WithServiceDiscoveryProviderHost(string serviceDiscoveryProviderHost)
{
_serviceDiscoveryProviderHost = serviceDiscoveryProviderHost;
return this;
}
public ServiceProviderConfiguraionBuilder WithServiceDiscoveryProviderPort(int serviceDiscoveryProviderPort)
{
_serviceDiscoveryProviderPort = serviceDiscoveryProviderPort;
return this;
}
public ServiceProviderConfiguraion Build()
{
return new ServiceProviderConfiguraion(_serviceName, _downstreamHost, _downstreamPort, _userServiceDiscovery,
_serviceDiscoveryProvider, _serviceDiscoveryProviderHost,_serviceDiscoveryProviderPort);
}
}
}

View File

@ -1,11 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Ocelot.Configuration.Builder;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using Ocelot.Configuration.Parser; using Ocelot.Configuration.Parser;
using Ocelot.Configuration.Validator; using Ocelot.Configuration.Validator;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Requester.QoS;
using Ocelot.Responses; using Ocelot.Responses;
using Ocelot.Utilities; using Ocelot.Utilities;
@ -21,36 +25,52 @@ namespace Ocelot.Configuration.Creator
private const string RegExMatchEverything = ".*"; private const string RegExMatchEverything = ".*";
private const string RegExMatchEndString = "$"; private const string RegExMatchEndString = "$";
private const string RegExIgnoreCase = "(?i)"; private const string RegExIgnoreCase = "(?i)";
private const string RegExForwardSlashOnly = "^/$";
private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser;
private readonly ILogger<FileOcelotConfigurationCreator> _logger; private readonly ILogger<FileOcelotConfigurationCreator> _logger;
private readonly ILoadBalancerFactory _loadBalanceFactory;
private readonly ILoadBalancerHouse _loadBalancerHouse;
private readonly IQoSProviderFactory _qoSProviderFactory;
private readonly IQosProviderHouse _qosProviderHouse;
public FileOcelotConfigurationCreator( public FileOcelotConfigurationCreator(
IOptions<FileConfiguration> options, IOptions<FileConfiguration> options,
IConfigurationValidator configurationValidator, IConfigurationValidator configurationValidator,
IClaimToThingConfigurationParser claimToThingConfigurationParser, IClaimToThingConfigurationParser claimToThingConfigurationParser,
ILogger<FileOcelotConfigurationCreator> logger) ILogger<FileOcelotConfigurationCreator> logger,
ILoadBalancerFactory loadBalancerFactory,
ILoadBalancerHouse loadBalancerHouse,
IQoSProviderFactory qoSProviderFactory,
IQosProviderHouse qosProviderHouse)
{ {
_loadBalanceFactory = loadBalancerFactory;
_loadBalancerHouse = loadBalancerHouse;
_qoSProviderFactory = qoSProviderFactory;
_qosProviderHouse = qosProviderHouse;
_options = options; _options = options;
_configurationValidator = configurationValidator; _configurationValidator = configurationValidator;
_claimToThingConfigurationParser = claimToThingConfigurationParser; _claimToThingConfigurationParser = claimToThingConfigurationParser;
_logger = logger; _logger = logger;
} }
public Response<IOcelotConfiguration> Create() public async Task<Response<IOcelotConfiguration>> Create()
{ {
var config = SetUpConfiguration(); var config = await SetUpConfiguration(_options.Value);
return new OkResponse<IOcelotConfiguration>(config); return new OkResponse<IOcelotConfiguration>(config);
} }
/// <summary> public async Task<Response<IOcelotConfiguration>> Create(FileConfiguration fileConfiguration)
/// This method is meant to be tempoary to convert a config to an ocelot config...probably wont keep this but we will see
/// will need a refactor at some point as its crap
/// </summary>
private IOcelotConfiguration SetUpConfiguration()
{ {
var response = _configurationValidator.IsValid(_options.Value); var config = await SetUpConfiguration(fileConfiguration);
return new OkResponse<IOcelotConfiguration>(config);
}
private async Task<IOcelotConfiguration> SetUpConfiguration(FileConfiguration fileConfiguration)
{
var response = _configurationValidator.IsValid(fileConfiguration);
if (response.Data.IsError) if (response.Data.IsError)
{ {
@ -66,59 +86,190 @@ namespace Ocelot.Configuration.Creator
var reRoutes = new List<ReRoute>(); var reRoutes = new List<ReRoute>();
foreach (var reRoute in _options.Value.ReRoutes) foreach (var reRoute in fileConfiguration.ReRoutes)
{ {
var ocelotReRoute = SetUpReRoute(reRoute, _options.Value.GlobalConfiguration); var ocelotReRoute = await SetUpReRoute(reRoute, fileConfiguration.GlobalConfiguration);
reRoutes.Add(ocelotReRoute); reRoutes.Add(ocelotReRoute);
} }
return new OcelotConfiguration(reRoutes); return new OcelotConfiguration(reRoutes, fileConfiguration.GlobalConfiguration.AdministrationPath);
} }
private ReRoute SetUpReRoute(FileReRoute reRoute, FileGlobalConfiguration globalConfiguration) private async Task<ReRoute> SetUpReRoute(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration)
{
var isAuthenticated = IsAuthenticated(fileReRoute);
var isAuthorised = IsAuthorised(fileReRoute);
var isCached = IsCached(fileReRoute);
var requestIdKey = BuildRequestId(fileReRoute, globalConfiguration);
var reRouteKey = BuildReRouteKey(fileReRoute);
var upstreamTemplatePattern = BuildUpstreamTemplatePattern(fileReRoute);
var isQos = IsQoS(fileReRoute);
var serviceProviderConfiguration = BuildServiceProviderConfiguration(fileReRoute, globalConfiguration);
var authOptionsForRoute = BuildAuthenticationOptions(fileReRoute);
var claimsToHeaders = BuildAddThingsToRequest(fileReRoute.AddHeadersToRequest);
var claimsToClaims = BuildAddThingsToRequest(fileReRoute.AddClaimsToRequest);
var claimsToQueries = BuildAddThingsToRequest(fileReRoute.AddQueriesToRequest);
var qosOptions = BuildQoSOptions(fileReRoute);
var enableRateLimiting = IsEnableRateLimiting(fileReRoute);
var rateLimitOption = BuildRateLimitOptions(fileReRoute, globalConfiguration, enableRateLimiting);
var reRoute = new ReRouteBuilder()
.WithDownstreamPathTemplate(fileReRoute.DownstreamPathTemplate)
.WithUpstreamPathTemplate(fileReRoute.UpstreamPathTemplate)
.WithUpstreamHttpMethod(fileReRoute.UpstreamHttpMethod)
.WithUpstreamTemplatePattern(upstreamTemplatePattern)
.WithIsAuthenticated(isAuthenticated)
.WithAuthenticationOptions(authOptionsForRoute)
.WithClaimsToHeaders(claimsToHeaders)
.WithClaimsToClaims(claimsToClaims)
.WithRouteClaimsRequirement(fileReRoute.RouteClaimsRequirement)
.WithIsAuthorised(isAuthorised)
.WithClaimsToQueries(claimsToQueries)
.WithRequestIdKey(requestIdKey)
.WithIsCached(isCached)
.WithCacheOptions(new CacheOptions(fileReRoute.FileCacheOptions.TtlSeconds))
.WithDownstreamScheme(fileReRoute.DownstreamScheme)
.WithLoadBalancer(fileReRoute.LoadBalancer)
.WithDownstreamHost(fileReRoute.DownstreamHost)
.WithDownstreamPort(fileReRoute.DownstreamPort)
.WithLoadBalancerKey(reRouteKey)
.WithServiceProviderConfiguraion(serviceProviderConfiguration)
.WithIsQos(isQos)
.WithQosOptions(qosOptions)
.WithEnableRateLimiting(enableRateLimiting)
.WithRateLimitOptions(rateLimitOption)
.Build();
await SetupLoadBalancer(reRoute);
SetupQosProvider(reRoute);
return reRoute;
}
private static RateLimitOptions BuildRateLimitOptions(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration, bool enableRateLimiting)
{
RateLimitOptions rateLimitOption = null;
if (enableRateLimiting)
{
rateLimitOption = new RateLimitOptions(enableRateLimiting, globalConfiguration.RateLimitOptions.ClientIdHeader,
fileReRoute.RateLimitOptions.ClientWhitelist, globalConfiguration.RateLimitOptions.DisableRateLimitHeaders,
globalConfiguration.RateLimitOptions.QuotaExceededMessage, globalConfiguration.RateLimitOptions.RateLimitCounterPrefix,
new RateLimitRule(fileReRoute.RateLimitOptions.Period, TimeSpan.FromSeconds(fileReRoute.RateLimitOptions.PeriodTimespan), fileReRoute.RateLimitOptions.Limit)
, globalConfiguration.RateLimitOptions.HttpStatusCode);
}
return rateLimitOption;
}
private static bool IsEnableRateLimiting(FileReRoute fileReRoute)
{
return (fileReRoute.RateLimitOptions != null && fileReRoute.RateLimitOptions.EnableRateLimiting) ? true : false;
}
private QoSOptions BuildQoSOptions(FileReRoute fileReRoute)
{
return new QoSOptionsBuilder()
.WithExceptionsAllowedBeforeBreaking(fileReRoute.QoSOptions.ExceptionsAllowedBeforeBreaking)
.WithDurationOfBreak(fileReRoute.QoSOptions.DurationOfBreak)
.WithTimeoutValue(fileReRoute.QoSOptions.TimeoutValue)
.Build();
}
private bool IsQoS(FileReRoute fileReRoute)
{
return fileReRoute.QoSOptions?.ExceptionsAllowedBeforeBreaking > 0 && fileReRoute.QoSOptions?.TimeoutValue > 0;
}
private bool IsAuthenticated(FileReRoute fileReRoute)
{
return !string.IsNullOrEmpty(fileReRoute.AuthenticationOptions?.Provider);
}
private bool IsAuthorised(FileReRoute fileReRoute)
{
return fileReRoute.RouteClaimsRequirement?.Count > 0;
}
private bool IsCached(FileReRoute fileReRoute)
{
return fileReRoute.FileCacheOptions.TtlSeconds > 0;
}
private string BuildRequestId(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration)
{ {
var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey); var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey);
var upstreamTemplate = BuildUpstreamTemplate(reRoute);
var isAuthenticated = !string.IsNullOrEmpty(reRoute.AuthenticationOptions?.Provider);
var isAuthorised = reRoute.RouteClaimsRequirement?.Count > 0;
var isCached = reRoute.FileCacheOptions.TtlSeconds > 0;
var requestIdKey = globalRequestIdConfiguration var requestIdKey = globalRequestIdConfiguration
? globalConfiguration.RequestIdKey ? globalConfiguration.RequestIdKey
: reRoute.RequestIdKey; : fileReRoute.RequestIdKey;
if (isAuthenticated) return requestIdKey;
{
var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider,
reRoute.AuthenticationOptions.ProviderRootUrl, reRoute.AuthenticationOptions.ScopeName,
reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes,
reRoute.AuthenticationOptions.ScopeSecret);
var claimsToHeaders = GetAddThingsToRequest(reRoute.AddHeadersToRequest);
var claimsToClaims = GetAddThingsToRequest(reRoute.AddClaimsToRequest);
var claimsToQueries = GetAddThingsToRequest(reRoute.AddQueriesToRequest);
return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate,
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
authOptionsForRoute, claimsToHeaders, claimsToClaims,
reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries,
requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds));
} }
return new ReRoute(reRoute.DownstreamTemplate, reRoute.UpstreamTemplate, private string BuildReRouteKey(FileReRoute fileReRoute)
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, {
null, new List<ClaimToThing>(), new List<ClaimToThing>(), //note - not sure if this is the correct key, but this is probably the only unique key i can think of given my poor brain
reRoute.RouteClaimsRequirement, isAuthorised, new List<ClaimToThing>(), var loadBalancerKey = $"{fileReRoute.UpstreamPathTemplate}{fileReRoute.UpstreamHttpMethod}";
requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds)); return loadBalancerKey;
} }
private string BuildUpstreamTemplate(FileReRoute reRoute) private AuthenticationOptions BuildAuthenticationOptions(FileReRoute fileReRoute)
{ {
var upstreamTemplate = reRoute.UpstreamTemplate; return new AuthenticationOptionsBuilder()
.WithProvider(fileReRoute.AuthenticationOptions?.Provider)
.WithProviderRootUrl(fileReRoute.AuthenticationOptions?.ProviderRootUrl)
.WithScopeName(fileReRoute.AuthenticationOptions?.ScopeName)
.WithRequireHttps(fileReRoute.AuthenticationOptions.RequireHttps)
.WithAdditionalScopes(fileReRoute.AuthenticationOptions?.AdditionalScopes)
.WithScopeSecret(fileReRoute.AuthenticationOptions?.ScopeSecret)
.Build();
}
private async Task SetupLoadBalancer(ReRoute reRoute)
{
var loadBalancer = await _loadBalanceFactory.Get(reRoute);
_loadBalancerHouse.Add(reRoute.ReRouteKey, loadBalancer);
}
private void SetupQosProvider(ReRoute reRoute)
{
var loadBalancer = _qoSProviderFactory.Get(reRoute);
_qosProviderHouse.Add(reRoute.ReRouteKey, loadBalancer);
}
private ServiceProviderConfiguraion BuildServiceProviderConfiguration(FileReRoute fileReRoute, FileGlobalConfiguration globalConfiguration)
{
var useServiceDiscovery = !string.IsNullOrEmpty(fileReRoute.ServiceName)
&& !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider);
var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0;
return new ServiceProviderConfiguraionBuilder()
.WithServiceName(fileReRoute.ServiceName)
.WithDownstreamHost(fileReRoute.DownstreamHost)
.WithDownstreamPort(fileReRoute.DownstreamPort)
.WithUseServiceDiscovery(useServiceDiscovery)
.WithServiceDiscoveryProvider(globalConfiguration?.ServiceDiscoveryProvider?.Provider)
.WithServiceDiscoveryProviderHost(globalConfiguration?.ServiceDiscoveryProvider?.Host)
.WithServiceDiscoveryProviderPort(serviceProviderPort)
.Build();
}
private string BuildUpstreamTemplatePattern(FileReRoute reRoute)
{
var upstreamTemplate = reRoute.UpstreamPathTemplate;
upstreamTemplate = upstreamTemplate.SetLastCharacterAs('/'); upstreamTemplate = upstreamTemplate.SetLastCharacterAs('/');
@ -140,6 +291,11 @@ namespace Ocelot.Configuration.Creator
upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything); upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything);
} }
if (upstreamTemplate == "/")
{
return RegExForwardSlashOnly;
}
var route = reRoute.ReRouteIsCaseSensitive var route = reRoute.ReRouteIsCaseSensitive
? $"{upstreamTemplate}{RegExMatchEndString}" ? $"{upstreamTemplate}{RegExMatchEndString}"
: $"{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; : $"{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}";
@ -147,7 +303,7 @@ namespace Ocelot.Configuration.Creator
return route; return route;
} }
private List<ClaimToThing> GetAddThingsToRequest(Dictionary<string,string> thingBeingAdded) private List<ClaimToThing> BuildAddThingsToRequest(Dictionary<string,string> thingBeingAdded)
{ {
var claimsToTHings = new List<ClaimToThing>(); var claimsToTHings = new List<ClaimToThing>();

View File

@ -1,9 +1,12 @@
using System.Threading.Tasks;
using Ocelot.Configuration.File;
using Ocelot.Responses; using Ocelot.Responses;
namespace Ocelot.Configuration.Creator namespace Ocelot.Configuration.Creator
{ {
public interface IOcelotConfigurationCreator public interface IOcelotConfigurationCreator
{ {
Response<IOcelotConfiguration> Create(); Task<Response<IOcelotConfiguration>> Create();
Task<Response<IOcelotConfiguration>> Create(FileConfiguration fileConfiguration);
} }
} }

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Models;
using Ocelot.Configuration.Provider;
namespace Ocelot.Configuration.Creator
{
public static class IdentityServerConfigurationCreator
{
public static IdentityServerConfiguration GetIdentityServerConfiguration()
{
var username = Environment.GetEnvironmentVariable("OCELOT_USERNAME");
var hash = Environment.GetEnvironmentVariable("OCELOT_HASH");
var salt = Environment.GetEnvironmentVariable("OCELOT_SALT");
return new IdentityServerConfiguration(
"admin",
false,
SupportedTokens.Both,
"secret",
new List<string> { "admin", "openid", "offline_access" },
"Ocelot Administration",
true,
GrantTypes.ResourceOwnerPassword,
AccessTokenType.Jwt,
false,
new List<User>
{
new User("admin", username, hash, salt)
}
);
}
}
}

View File

@ -1,7 +1,19 @@
namespace Ocelot.Configuration.File 
namespace Ocelot.Configuration.File
{ {
public class FileGlobalConfiguration public class FileGlobalConfiguration
{ {
public FileGlobalConfiguration()
{
ServiceDiscoveryProvider = new FileServiceDiscoveryProvider();
RateLimitOptions = new FileRateLimitOptions();
}
public string RequestIdKey { get; set; } public string RequestIdKey { get; set; }
public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;}
public string AdministrationPath {get;set;}
public FileRateLimitOptions RateLimitOptions { get; set; }
} }
} }

View File

@ -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
}
}

View File

@ -0,0 +1,11 @@
namespace Ocelot.Configuration.File
{
public class FileQoSOptions
{
public int ExceptionsAllowedBeforeBreaking { get; set; }
public int DurationOfBreak { get; set; }
public int TimeoutValue { get; set; }
}
}

View 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;
}
}

View 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; }
}
}

View File

@ -12,10 +12,12 @@ namespace Ocelot.Configuration.File
AddQueriesToRequest = new Dictionary<string, string>(); AddQueriesToRequest = new Dictionary<string, string>();
AuthenticationOptions = new FileAuthenticationOptions(); AuthenticationOptions = new FileAuthenticationOptions();
FileCacheOptions = new FileCacheOptions(); FileCacheOptions = new FileCacheOptions();
QoSOptions = new FileQoSOptions();
RateLimitOptions = new FileRateLimitRule();
} }
public string DownstreamTemplate { get; set; } public string DownstreamPathTemplate { get; set; }
public string UpstreamTemplate { get; set; } public string UpstreamPathTemplate { get; set; }
public string UpstreamHttpMethod { get; set; } public string UpstreamHttpMethod { get; set; }
public FileAuthenticationOptions AuthenticationOptions { get; set; } public FileAuthenticationOptions AuthenticationOptions { get; set; }
public Dictionary<string, string> AddHeadersToRequest { get; set; } public Dictionary<string, string> AddHeadersToRequest { get; set; }
@ -25,5 +27,12 @@ namespace Ocelot.Configuration.File
public string RequestIdKey { get; set; } public string RequestIdKey { get; set; }
public FileCacheOptions FileCacheOptions { get; set; } public FileCacheOptions FileCacheOptions { get; set; }
public bool ReRouteIsCaseSensitive { get; set; } public bool ReRouteIsCaseSensitive { get; set; }
public string ServiceName { get; set; }
public string DownstreamScheme {get;set;}
public string DownstreamHost {get;set;}
public int DownstreamPort { get; set; }
public FileQoSOptions QoSOptions { get; set; }
public string LoadBalancer {get;set;}
public FileRateLimitRule RateLimitOptions { get; set; }
} }
} }

View File

@ -0,0 +1,10 @@
namespace Ocelot.Configuration.File
{
public class FileServiceDiscoveryProvider
{
public string Provider {get;set;}
public string Host {get;set;}
public int Port { get; set; }
}
}

View File

@ -5,5 +5,6 @@ namespace Ocelot.Configuration
public interface IOcelotConfiguration public interface IOcelotConfiguration
{ {
List<ReRoute> ReRoutes { get; } List<ReRoute> ReRoutes { get; }
string AdministrationPath {get;}
} }
} }

View File

@ -4,11 +4,13 @@ namespace Ocelot.Configuration
{ {
public class OcelotConfiguration : IOcelotConfiguration public class OcelotConfiguration : IOcelotConfiguration
{ {
public OcelotConfiguration(List<ReRoute> reRoutes) public OcelotConfiguration(List<ReRoute> reRoutes, string administrationPath)
{ {
ReRoutes = reRoutes; ReRoutes = reRoutes;
AdministrationPath = administrationPath;
} }
public List<ReRoute> ReRoutes { get; } public List<ReRoute> ReRoutes { get; }
public string AdministrationPath {get;}
} }
} }

View File

@ -0,0 +1,25 @@
using System;
using System.IO;
using Newtonsoft.Json;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Repository;
using Ocelot.Responses;
namespace Ocelot.Configuration.Provider
{
public class FileConfigurationProvider : IFileConfigurationProvider
{
private IFileConfigurationRepository _repo;
public FileConfigurationProvider(IFileConfigurationRepository repo)
{
_repo = repo;
}
public Response<FileConfiguration> Get()
{
var fileConfig = _repo.Get();
return new OkResponse<FileConfiguration>(fileConfig.Data);
}
}
}

View File

@ -0,0 +1,10 @@
using Ocelot.Configuration.File;
using Ocelot.Responses;
namespace Ocelot.Configuration.Provider
{
public interface IFileConfigurationProvider
{
Response<FileConfiguration> Get();
}
}

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Models;
namespace Ocelot.Configuration.Provider
{
public interface IIdentityServerConfiguration
{
string ApiName { get; }
bool RequireHttps { get; }
List<string> AllowedScopes { get; }
SupportedTokens SupportedTokens { get; }
string ApiSecret { get; }
string Description {get;}
bool Enabled {get;}
IEnumerable<string> AllowedGrantTypes {get;}
AccessTokenType AccessTokenType {get;}
bool RequireClientSecret {get;}
List<User> Users {get;}
}
}

View File

@ -0,0 +1,47 @@
using System.Collections.Generic;
using IdentityServer4.AccessTokenValidation;
using IdentityServer4.Models;
namespace Ocelot.Configuration.Provider
{
public class IdentityServerConfiguration : IIdentityServerConfiguration
{
public IdentityServerConfiguration(
string apiName,
bool requireHttps,
SupportedTokens supportedTokens,
string apiSecret,
List<string> allowedScopes,
string description,
bool enabled,
IEnumerable<string> grantType,
AccessTokenType accessTokenType,
bool requireClientSecret,
List<User> users)
{
ApiName = apiName;
RequireHttps = requireHttps;
SupportedTokens = supportedTokens;
ApiSecret = apiSecret;
AllowedScopes = allowedScopes;
Description = description;
Enabled = enabled;
AllowedGrantTypes = grantType;
AccessTokenType = accessTokenType;
RequireClientSecret = requireClientSecret;
Users = users;
}
public string ApiName { get; private set; }
public bool RequireHttps { get; private set; }
public List<string> AllowedScopes { get; private set; }
public SupportedTokens SupportedTokens { get; private set; }
public string ApiSecret { get; private set; }
public string Description {get;private set;}
public bool Enabled {get;private set;}
public IEnumerable<string> AllowedGrantTypes {get;private set;}
public AccessTokenType AccessTokenType {get;private set;}
public bool RequireClientSecret {get;private set;}
public List<User> Users {get;private set;}
}
}

View File

@ -1,5 +1,4 @@
using Ocelot.Configuration.Creator; using Ocelot.Configuration.Repository;
using Ocelot.Configuration.Repository;
using Ocelot.Responses; using Ocelot.Responses;
namespace Ocelot.Configuration.Provider namespace Ocelot.Configuration.Provider
@ -10,13 +9,10 @@ namespace Ocelot.Configuration.Provider
public class OcelotConfigurationProvider : IOcelotConfigurationProvider public class OcelotConfigurationProvider : IOcelotConfigurationProvider
{ {
private readonly IOcelotConfigurationRepository _repo; private readonly IOcelotConfigurationRepository _repo;
private readonly IOcelotConfigurationCreator _creator;
public OcelotConfigurationProvider(IOcelotConfigurationRepository repo, public OcelotConfigurationProvider(IOcelotConfigurationRepository repo)
IOcelotConfigurationCreator creator)
{ {
_repo = repo; _repo = repo;
_creator = creator;
} }
public Response<IOcelotConfiguration> Get() public Response<IOcelotConfiguration> Get()
@ -28,20 +24,6 @@ namespace Ocelot.Configuration.Provider
return new ErrorResponse<IOcelotConfiguration>(repoConfig.Errors); return new ErrorResponse<IOcelotConfiguration>(repoConfig.Errors);
} }
if (repoConfig.Data == null)
{
var creatorConfig = _creator.Create();
if (creatorConfig.IsError)
{
return new ErrorResponse<IOcelotConfiguration>(creatorConfig.Errors);
}
_repo.AddOrReplace(creatorConfig.Data);
return new OkResponse<IOcelotConfiguration>(creatorConfig.Data);
}
return new OkResponse<IOcelotConfiguration>(repoConfig.Data); return new OkResponse<IOcelotConfiguration>(repoConfig.Data);
} }
} }

View File

@ -0,0 +1,17 @@
namespace Ocelot.Configuration.Provider
{
public class User
{
public User(string subject, string userName, string hash, string salt)
{
Subject = subject;
UserName = userName;
Hash = hash;
Salt = salt;
}
public string Subject { get; private set; }
public string UserName { get; private set; }
public string Hash { get; private set; }
public string Salt { get; private set; }
}
}

View File

@ -0,0 +1,30 @@
using System;
using Polly.Timeout;
namespace Ocelot.Configuration
{
public class QoSOptions
{
public QoSOptions(
int exceptionsAllowedBeforeBreaking,
int durationofBreak,
int timeoutValue,
TimeoutStrategy timeoutStrategy = TimeoutStrategy.Pessimistic)
{
ExceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking;
DurationOfBreak = TimeSpan.FromMilliseconds(durationofBreak);
TimeoutValue = TimeSpan.FromMilliseconds(timeoutValue);
TimeoutStrategy = timeoutStrategy;
}
public int ExceptionsAllowedBeforeBreaking { get; private set; }
public TimeSpan DurationOfBreak { get; private set; }
public TimeSpan TimeoutValue { get; private set; }
public TimeoutStrategy TimeoutStrategy { get; private set; }
}
}

View 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; }
}
}

View File

@ -1,16 +1,43 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http;
using Ocelot.Values;
namespace Ocelot.Configuration namespace Ocelot.Configuration
{ {
public class ReRoute public class ReRoute
{ {
public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, public ReRoute(PathTemplate downstreamPathTemplate,
bool isAuthenticated, AuthenticationOptions authenticationOptions, List<ClaimToThing> configurationHeaderExtractorProperties, PathTemplate upstreamTemplate,
List<ClaimToThing> claimsToClaims, Dictionary<string, string> routeClaimsRequirement, bool isAuthorised, List<ClaimToThing> claimsToQueries, HttpMethod upstreamHttpMethod,
string requestIdKey, bool isCached, CacheOptions fileCacheOptions) string upstreamTemplatePattern,
bool isAuthenticated,
AuthenticationOptions authenticationOptions,
List<ClaimToThing> configurationHeaderExtractorProperties,
List<ClaimToThing> claimsToClaims,
Dictionary<string, string> routeClaimsRequirement,
bool isAuthorised,
List<ClaimToThing> claimsToQueries,
string requestIdKey,
bool isCached,
CacheOptions fileCacheOptions,
string downstreamScheme,
string loadBalancer,
string downstreamHost,
int downstreamPort,
string reRouteKey,
ServiceProviderConfiguraion serviceProviderConfiguraion,
bool isQos,
QoSOptions qos,
bool enableRateLimit,
RateLimitOptions ratelimitOptions)
{ {
DownstreamTemplate = downstreamTemplate; ReRouteKey = reRouteKey;
UpstreamTemplate = upstreamTemplate; ServiceProviderConfiguraion = serviceProviderConfiguraion;
LoadBalancer = loadBalancer;
DownstreamHost = downstreamHost;
DownstreamPort = downstreamPort;
DownstreamPathTemplate = downstreamPathTemplate;
UpstreamPathTemplate = upstreamTemplate;
UpstreamHttpMethod = upstreamHttpMethod; UpstreamHttpMethod = upstreamHttpMethod;
UpstreamTemplatePattern = upstreamTemplatePattern; UpstreamTemplatePattern = upstreamTemplatePattern;
IsAuthenticated = isAuthenticated; IsAuthenticated = isAuthenticated;
@ -26,12 +53,18 @@ namespace Ocelot.Configuration
?? new List<ClaimToThing>(); ?? new List<ClaimToThing>();
ClaimsToHeaders = configurationHeaderExtractorProperties ClaimsToHeaders = configurationHeaderExtractorProperties
?? new List<ClaimToThing>(); ?? new List<ClaimToThing>();
DownstreamScheme = downstreamScheme;
IsQos = isQos;
QosOptions = qos;
EnableEndpointRateLimiting = enableRateLimit;
RateLimitOptions = ratelimitOptions;
} }
public string DownstreamTemplate { get; private set; } public string ReRouteKey {get;private set;}
public string UpstreamTemplate { get; private set; } public PathTemplate DownstreamPathTemplate { get; private set; }
public PathTemplate UpstreamPathTemplate { get; private set; }
public string UpstreamTemplatePattern { get; private set; } public string UpstreamTemplatePattern { get; private set; }
public string UpstreamHttpMethod { get; private set; } public HttpMethod UpstreamHttpMethod { get; private set; }
public bool IsAuthenticated { get; private set; } public bool IsAuthenticated { get; private set; }
public bool IsAuthorised { get; private set; } public bool IsAuthorised { get; private set; }
public AuthenticationOptions AuthenticationOptions { get; private set; } public AuthenticationOptions AuthenticationOptions { get; private set; }
@ -42,5 +75,14 @@ namespace Ocelot.Configuration
public string RequestIdKey { get; private set; } public string RequestIdKey { get; private set; }
public bool IsCached { get; private set; } public bool IsCached { get; private set; }
public CacheOptions FileCacheOptions { get; private set; } public CacheOptions FileCacheOptions { get; private set; }
public string DownstreamScheme {get;private set;}
public bool IsQos { get; private set; }
public QoSOptions QosOptions { get; private set; }
public string LoadBalancer {get;private set;}
public string DownstreamHost { get; private set; }
public int DownstreamPort { get; private set; }
public ServiceProviderConfiguraion ServiceProviderConfiguraion { get; private set; }
public bool EnableEndpointRateLimiting { get; private set; }
public RateLimitOptions RateLimitOptions { get; private set; }
} }
} }

View File

@ -0,0 +1,42 @@
using System;
using Newtonsoft.Json;
using Ocelot.Configuration.File;
using Ocelot.Responses;
namespace Ocelot.Configuration.Repository
{
public class FileConfigurationRepository : IFileConfigurationRepository
{
private static readonly object _lock = new object();
public Response<FileConfiguration> Get()
{
var configFilePath = $"{AppContext.BaseDirectory}/configuration.json";
string json = string.Empty;
lock(_lock)
{
json = System.IO.File.ReadAllText(configFilePath);
}
var fileConfiguration = JsonConvert.DeserializeObject<FileConfiguration>(json);
return new OkResponse<FileConfiguration>(fileConfiguration);
}
public Response Set(FileConfiguration fileConfiguration)
{
var configurationPath = $"{AppContext.BaseDirectory}/configuration.json";
var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration);
lock(_lock)
{
if (System.IO.File.Exists(configurationPath))
{
System.IO.File.Delete(configurationPath);
}
System.IO.File.WriteAllText(configurationPath, jsonConfiguration);
}
return new OkResponse();
}
}
}

View File

@ -0,0 +1,11 @@
using Ocelot.Configuration.File;
using Ocelot.Responses;
namespace Ocelot.Configuration.Repository
{
public interface IFileConfigurationRepository
{
Response<FileConfiguration> Get();
Response Set(FileConfiguration fileConfiguration);
}
}

View File

@ -0,0 +1,25 @@
namespace Ocelot.Configuration
{
public class ServiceProviderConfiguraion
{
public ServiceProviderConfiguraion(string serviceName, string downstreamHost,
int downstreamPort, bool useServiceDiscovery, string serviceDiscoveryProvider, string serviceProviderHost, int serviceProviderPort)
{
ServiceName = serviceName;
DownstreamHost = downstreamHost;
DownstreamPort = downstreamPort;
UseServiceDiscovery = useServiceDiscovery;
ServiceDiscoveryProvider = serviceDiscoveryProvider;
ServiceProviderHost = serviceProviderHost;
ServiceProviderPort = serviceProviderPort;
}
public string ServiceName { get; }
public string DownstreamHost { get; }
public int DownstreamPort { get; }
public bool UseServiceDiscovery { get; }
public string ServiceDiscoveryProvider { get; }
public string ServiceProviderHost { get; private set; }
public int ServiceProviderPort { get; private set; }
}
}

View File

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using Ocelot.Configuration.Creator;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Repository;
using Ocelot.Responses;
namespace Ocelot.Configuration.Setter
{
public class FileConfigurationSetter : IFileConfigurationSetter
{
private readonly IOcelotConfigurationRepository _configRepo;
private readonly IOcelotConfigurationCreator _configCreator;
private readonly IFileConfigurationRepository _repo;
public FileConfigurationSetter(IOcelotConfigurationRepository configRepo,
IOcelotConfigurationCreator configCreator, IFileConfigurationRepository repo)
{
_configRepo = configRepo;
_configCreator = configCreator;
_repo = repo;
}
public async Task<Response> Set(FileConfiguration fileConfig)
{
var response = _repo.Set(fileConfig);
if(response.IsError)
{
return new ErrorResponse(response.Errors);
}
var config = await _configCreator.Create(fileConfig);
if(!config.IsError)
{
_configRepo.AddOrReplace(config.Data);
}
return new ErrorResponse(config.Errors);
}
}
}

View File

@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Ocelot.Configuration.File;
using Ocelot.Responses;
namespace Ocelot.Configuration.Setter
{
public interface IFileConfigurationSetter
{
Task<Response> Set(FileConfiguration config);
}
}

View File

@ -0,0 +1,11 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class DownstreamPathTemplateAlreadyUsedError : Error
{
public DownstreamPathTemplateAlreadyUsedError(string message) : base(message, OcelotErrorCode.DownstreampathTemplateAlreadyUsedError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class DownstreamPathTemplateContainsSchemeError : Error
{
public DownstreamPathTemplateContainsSchemeError(string message)
: base(message, OcelotErrorCode.DownstreamPathTemplateContainsSchemeError)
{
}
}
}

View File

@ -1,11 +0,0 @@
using Ocelot.Errors;
namespace Ocelot.Configuration.Validator
{
public class DownstreamTemplateAlreadyUsedError : Error
{
public DownstreamTemplateAlreadyUsedError(string message) : base(message, OcelotErrorCode.DownstreamTemplateAlreadyUsedError)
{
}
}
}

View File

@ -26,6 +26,13 @@ namespace Ocelot.Configuration.Validator
return new OkResponse<ConfigurationValidationResult>(result); return new OkResponse<ConfigurationValidationResult>(result);
} }
result = CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(configuration);
if (result.IsError)
{
return new OkResponse<ConfigurationValidationResult>(result);
}
return new OkResponse<ConfigurationValidationResult>(result); return new OkResponse<ConfigurationValidationResult>(result);
} }
@ -47,7 +54,7 @@ namespace Ocelot.Configuration.Validator
continue; continue;
} }
var error = new UnsupportedAuthenticationProviderError($"{reRoute.AuthenticationOptions?.Provider} is unsupported authentication provider, upstream template is {reRoute.UpstreamTemplate}, upstream method is {reRoute.UpstreamHttpMethod}"); var error = new UnsupportedAuthenticationProviderError($"{reRoute.AuthenticationOptions?.Provider} is unsupported authentication provider, upstream template is {reRoute.UpstreamPathTemplate}, upstream method is {reRoute.UpstreamHttpMethod}");
errors.Add(error); errors.Add(error);
} }
@ -63,21 +70,42 @@ namespace Ocelot.Configuration.Validator
return Enum.TryParse(provider, true, out supportedProvider); return Enum.TryParse(provider, true, out supportedProvider);
} }
private ConfigurationValidationResult CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(FileConfiguration configuration)
{
var errors = new List<Error>();
foreach(var reRoute in configuration.ReRoutes)
{
if(reRoute.DownstreamPathTemplate.Contains("https://")
|| reRoute.DownstreamPathTemplate.Contains("http://"))
{
errors.Add(new DownstreamPathTemplateContainsSchemeError($"{reRoute.DownstreamPathTemplate} contains scheme"));
}
}
if(errors.Any())
{
return new ConfigurationValidationResult(true, errors);
}
return new ConfigurationValidationResult(false, errors);
}
private ConfigurationValidationResult CheckForDupliateReRoutes(FileConfiguration configuration) private ConfigurationValidationResult CheckForDupliateReRoutes(FileConfiguration configuration)
{ {
var hasDupes = configuration.ReRoutes var hasDupes = configuration.ReRoutes
.GroupBy(x => new { x.UpstreamTemplate, x.UpstreamHttpMethod }).Any(x => x.Skip(1).Any()); .GroupBy(x => new { x.UpstreamPathTemplate, x.UpstreamHttpMethod }).Any(x => x.Skip(1).Any());
if (!hasDupes) if (!hasDupes)
{ {
return new ConfigurationValidationResult(false); return new ConfigurationValidationResult(false);
} }
var dupes = configuration.ReRoutes.GroupBy(x => new { x.UpstreamTemplate, x.UpstreamHttpMethod }) var dupes = configuration.ReRoutes.GroupBy(x => new { x.UpstreamPathTemplate, x.UpstreamHttpMethod })
.Where(x => x.Skip(1).Any()); .Where(x => x.Skip(1).Any());
var errors = dupes var errors = dupes
.Select(d => new DownstreamTemplateAlreadyUsedError(string.Format("Duplicate DownstreamTemplate: {0}", d.Key.UpstreamTemplate))) .Select(d => new DownstreamPathTemplateAlreadyUsedError(string.Format("Duplicate DownstreamPath: {0}", d.Key.UpstreamPathTemplate)))
.Cast<Error>() .Cast<Error>()
.ToList(); .ToList();

View File

@ -0,0 +1,49 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Provider;
using Ocelot.Configuration.Setter;
namespace Ocelot.Controllers
{
[Authorize]
[Route("configuration")]
public class FileConfigurationController : Controller
{
private readonly IFileConfigurationProvider _configGetter;
private readonly IFileConfigurationSetter _configSetter;
public FileConfigurationController(IFileConfigurationProvider getFileConfig, IFileConfigurationSetter configSetter)
{
_configGetter = getFileConfig;
_configSetter = configSetter;
}
[HttpGet]
public IActionResult Get()
{
var response = _configGetter.Get();
if(response.IsError)
{
return new BadRequestObjectResult(response.Errors);
}
return new OkObjectResult(response.Data);
}
[HttpPost]
public async Task<IActionResult> Post([FromBody]FileConfiguration fileConfiguration)
{
var response = await _configSetter.Set(fileConfiguration);
if(response.IsError)
{
return new BadRequestObjectResult(response.Errors);
}
return new OkObjectResult(fileConfiguration);
}
}
}

View File

@ -1,64 +1,121 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using CacheManager.Core; using CacheManager.Core;
using IdentityServer4.Models;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Ocelot.Authentication.Handler.Creator; using Ocelot.Authentication.Handler.Creator;
using Ocelot.Authentication.Handler.Factory; using Ocelot.Authentication.Handler.Factory;
using Ocelot.Authorisation; using Ocelot.Authorisation;
using Ocelot.Cache; using Ocelot.Cache;
using Ocelot.Claims; using Ocelot.Claims;
using Ocelot.Configuration.Authentication;
using Ocelot.Configuration.Creator; using Ocelot.Configuration.Creator;
using Ocelot.Configuration.File; using Ocelot.Configuration.File;
using Ocelot.Configuration.Parser; using Ocelot.Configuration.Parser;
using Ocelot.Configuration.Provider; using Ocelot.Configuration.Provider;
using Ocelot.Configuration.Repository; using Ocelot.Configuration.Repository;
using Ocelot.Configuration.Setter;
using Ocelot.Configuration.Validator; using Ocelot.Configuration.Validator;
using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.Finder;
using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.DownstreamUrlCreator;
using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Headers; using Ocelot.Headers;
using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Infrastructure.Claims.Parser;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.LoadBalancer.LoadBalancers;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.Middleware;
using Ocelot.QueryStrings; using Ocelot.QueryStrings;
using Ocelot.Request.Builder; using Ocelot.Request.Builder;
using Ocelot.Requester; using Ocelot.Requester;
using Ocelot.Requester.QoS;
using Ocelot.Responder; using Ocelot.Responder;
using Ocelot.ServiceDiscovery;
using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider;
using Ocelot.RateLimit;
namespace Ocelot.DependencyInjection namespace Ocelot.DependencyInjection
{ {
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddOcelotOutputCaching(this IServiceCollection services, Action<ConfigurationBuilderCachePart> settings) public static IServiceCollection AddOcelotOutputCaching(this IServiceCollection services, Action<ConfigurationBuilderCachePart> settings)
{ {
var cacheManagerOutputCache = CacheFactory.Build<HttpResponseMessage>("OcelotOutputCache", settings); var cacheManagerOutputCache = CacheFactory.Build<HttpResponseMessage>("OcelotOutputCache", settings);
var ocelotCacheManager = new OcelotCacheManagerCache<HttpResponseMessage>(cacheManagerOutputCache); var ocelotCacheManager = new OcelotCacheManagerCache<HttpResponseMessage>(cacheManagerOutputCache);
services.AddSingleton<ICacheManager<HttpResponseMessage>>(cacheManagerOutputCache); services.AddSingleton<ICacheManager<HttpResponseMessage>>(cacheManagerOutputCache);
services.AddSingleton<IOcelotCache<HttpResponseMessage>>(ocelotCacheManager); services.AddSingleton<IOcelotCache<HttpResponseMessage>>(ocelotCacheManager);
return services; return services;
} }
public static IServiceCollection AddOcelotFileConfiguration(this IServiceCollection services, IConfigurationRoot configurationRoot)
public static IServiceCollection AddOcelot(this IServiceCollection services, IConfigurationRoot configurationRoot)
{ {
services.Configure<FileConfiguration>(configurationRoot); services.Configure<FileConfiguration>(configurationRoot);
services.AddSingleton<IOcelotConfigurationCreator, FileOcelotConfigurationCreator>(); services.AddSingleton<IOcelotConfigurationCreator, FileOcelotConfigurationCreator>();
services.AddSingleton<IOcelotConfigurationRepository, InMemoryOcelotConfigurationRepository>(); services.AddSingleton<IOcelotConfigurationRepository, InMemoryOcelotConfigurationRepository>();
services.AddSingleton<IConfigurationValidator, FileConfigurationValidator>(); services.AddSingleton<IConfigurationValidator, FileConfigurationValidator>();
services.AddSingleton<IBaseUrlFinder, BaseUrlFinder>();
return services; 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>();
} }
public static IServiceCollection AddOcelot(this IServiceCollection services) services.AddMvcCore()
{ .AddAuthorization()
services.AddMvcCore().AddJsonFormatters(); .AddJsonFormatters();
services.AddLogging(); 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<IOcelotLoggerFactory, AspDotNetLoggerFactory>();
services.AddSingleton<IUrlBuilder, UrlBuilder>();
services.AddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>(); services.AddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>();
services.AddSingleton<IOcelotConfigurationProvider, OcelotConfigurationProvider>(); services.AddSingleton<IOcelotConfigurationProvider, OcelotConfigurationProvider>();
services.AddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>(); services.AddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>();
@ -69,7 +126,7 @@ namespace Ocelot.DependencyInjection
services.AddSingleton<IClaimsParser, ClaimsParser>(); services.AddSingleton<IClaimsParser, ClaimsParser>();
services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>(); services.AddSingleton<IUrlPathToUrlTemplateMatcher, RegExUrlMatcher>();
services.AddSingleton<IUrlPathPlaceholderNameAndValueFinder, UrlPathPlaceholderNameAndValueFinder>(); services.AddSingleton<IUrlPathPlaceholderNameAndValueFinder, UrlPathPlaceholderNameAndValueFinder>();
services.AddSingleton<IDownstreamUrlPathPlaceholderReplacer, DownstreamUrlPathPlaceholderReplacer>(); services.AddSingleton<IDownstreamPathPlaceholderReplacer, DownstreamTemplatePathPlaceholderReplacer>();
services.AddSingleton<IDownstreamRouteFinder, DownstreamRouteFinder.Finder.DownstreamRouteFinder>(); services.AddSingleton<IDownstreamRouteFinder, DownstreamRouteFinder.Finder.DownstreamRouteFinder>();
services.AddSingleton<IHttpRequester, HttpClientHttpRequester>(); services.AddSingleton<IHttpRequester, HttpClientHttpRequester>();
services.AddSingleton<IHttpResponder, HttpContextResponder>(); services.AddSingleton<IHttpResponder, HttpContextResponder>();
@ -77,12 +134,13 @@ namespace Ocelot.DependencyInjection
services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>(); services.AddSingleton<IErrorsToHttpStatusCodeMapper, ErrorsToHttpStatusCodeMapper>();
services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>(); services.AddSingleton<IAuthenticationHandlerFactory, AuthenticationHandlerFactory>();
services.AddSingleton<IAuthenticationHandlerCreator, AuthenticationHandlerCreator>(); 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 // 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 // could maybe use a scoped data repository
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<IRequestScopedDataRepository, HttpDataRepository>(); services.AddScoped<IRequestScopedDataRepository, HttpDataRepository>();
services.AddMemoryCache();
return services; return services;
} }
} }

View File

@ -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;
}
}
}

View File

@ -25,15 +25,22 @@ namespace Ocelot.DownstreamRouteFinder.Finder
{ {
var configuration = _configProvider.Get(); var configuration = _configProvider.Get();
var applicableReRoutes = configuration.Data.ReRoutes.Where(r => string.Equals(r.UpstreamHttpMethod, upstreamHttpMethod, StringComparison.CurrentCultureIgnoreCase)); var applicableReRoutes = configuration.Data.ReRoutes.Where(r => string.Equals(r.UpstreamHttpMethod.Method, upstreamHttpMethod, StringComparison.CurrentCultureIgnoreCase));
foreach (var reRoute in applicableReRoutes) foreach (var reRoute in applicableReRoutes)
{ {
if (upstreamUrlPath == reRoute.UpstreamTemplatePattern)
{
var templateVariableNameAndValues = _urlPathPlaceholderNameAndValueFinder.Find(upstreamUrlPath, reRoute.UpstreamPathTemplate.Value);
return new OkResponse<DownstreamRoute>(new DownstreamRoute(templateVariableNameAndValues.Data, reRoute));
}
var urlMatch = _urlMatcher.Match(upstreamUrlPath, reRoute.UpstreamTemplatePattern); var urlMatch = _urlMatcher.Match(upstreamUrlPath, reRoute.UpstreamTemplatePattern);
if (urlMatch.Data.Match) if (urlMatch.Data.Match)
{ {
var templateVariableNameAndValues = _urlPathPlaceholderNameAndValueFinder.Find(upstreamUrlPath, reRoute.UpstreamTemplate); var templateVariableNameAndValues = _urlPathPlaceholderNameAndValueFinder.Find(upstreamUrlPath, reRoute.UpstreamPathTemplate.Value);
return new OkResponse<DownstreamRoute>(new DownstreamRoute(templateVariableNameAndValues.Data, reRoute)); return new OkResponse<DownstreamRoute>(new DownstreamRoute(templateVariableNameAndValues.Data, reRoute));
} }

View File

@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.Finder;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging; using Ocelot.Logging;
@ -44,7 +43,7 @@ namespace Ocelot.DownstreamRouteFinder.Middleware
return; return;
} }
_logger.LogDebug("downstream template is {downstreamRoute.Data.ReRoute.DownstreamTemplate}", downstreamRoute.Data.ReRoute.DownstreamTemplate); _logger.LogDebug("downstream template is {downstreamRoute.Data.ReRoute.DownstreamPath}", downstreamRoute.Data.ReRoute.DownstreamPathTemplate);
SetDownstreamRouteForThisRequest(downstreamRoute.Data); SetDownstreamRouteForThisRequest(downstreamRoute.Data);

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.DownstreamUrlCreator
{
public class DownstreamHostNullOrEmptyError : Error
{
public DownstreamHostNullOrEmptyError()
: base("downstream host was null or empty", OcelotErrorCode.DownstreamHostNullOrEmptyError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.DownstreamUrlCreator
{
public class DownstreamPathNullOrEmptyError : Error
{
public DownstreamPathNullOrEmptyError()
: base("downstream path was null or empty", OcelotErrorCode.DownstreamPathNullOrEmptyError)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.DownstreamUrlCreator
{
public class DownstreamSchemeNullOrEmptyError : Error
{
public DownstreamSchemeNullOrEmptyError()
: base("downstream scheme was null or empty", OcelotErrorCode.DownstreamSchemeNullOrEmptyError)
{
}
}
}

View File

@ -0,0 +1,10 @@
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator
{
public interface IUrlBuilder
{
Response<DownstreamUrl> Build(string downstreamPath, string downstreamScheme, HostAndPort downstreamHostAndPort);
}
}

View File

@ -1,6 +1,5 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging; using Ocelot.Logging;
@ -11,17 +10,20 @@ namespace Ocelot.DownstreamUrlCreator.Middleware
public class DownstreamUrlCreatorMiddleware : OcelotMiddleware public class DownstreamUrlCreatorMiddleware : OcelotMiddleware
{ {
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly IDownstreamUrlPathPlaceholderReplacer _urlReplacer; private readonly IDownstreamPathPlaceholderReplacer _replacer;
private readonly IOcelotLogger _logger; private readonly IOcelotLogger _logger;
private readonly IUrlBuilder _urlBuilder;
public DownstreamUrlCreatorMiddleware(RequestDelegate next, public DownstreamUrlCreatorMiddleware(RequestDelegate next,
IOcelotLoggerFactory loggerFactory, IOcelotLoggerFactory loggerFactory,
IDownstreamUrlPathPlaceholderReplacer urlReplacer, IDownstreamPathPlaceholderReplacer replacer,
IRequestScopedDataRepository requestScopedDataRepository) IRequestScopedDataRepository requestScopedDataRepository,
IUrlBuilder urlBuilder)
:base(requestScopedDataRepository) :base(requestScopedDataRepository)
{ {
_next = next; _next = next;
_urlReplacer = urlReplacer; _replacer = replacer;
_urlBuilder = urlBuilder;
_logger = loggerFactory.CreateLogger<DownstreamUrlCreatorMiddleware>(); _logger = loggerFactory.CreateLogger<DownstreamUrlCreatorMiddleware>();
} }
@ -29,19 +31,34 @@ namespace Ocelot.DownstreamUrlCreator.Middleware
{ {
_logger.LogDebug("started calling downstream url creator middleware"); _logger.LogDebug("started calling downstream url creator middleware");
var downstreamUrl = _urlReplacer.Replace(DownstreamRoute.ReRoute.DownstreamTemplate, DownstreamRoute.TemplatePlaceholderNameAndValues); var dsPath = _replacer
.Replace(DownstreamRoute.ReRoute.DownstreamPathTemplate, DownstreamRoute.TemplatePlaceholderNameAndValues);
if (downstreamUrl.IsError) if (dsPath.IsError)
{ {
_logger.LogDebug("IDownstreamUrlPathPlaceholderReplacer returned an error, setting pipeline error"); _logger.LogDebug("IDownstreamPathPlaceholderReplacer returned an error, setting pipeline error");
SetPipelineError(downstreamUrl.Errors); SetPipelineError(dsPath.Errors);
return; return;
} }
_logger.LogDebug("downstream url is {downstreamUrl.Data.Value}", downstreamUrl.Data.Value); var dsScheme = DownstreamRoute.ReRoute.DownstreamScheme;
SetDownstreamUrlForThisRequest(downstreamUrl.Data.Value); var dsHostAndPort = HostAndPort;
var dsUrl = _urlBuilder.Build(dsPath.Data.Value, dsScheme, dsHostAndPort);
if (dsUrl.IsError)
{
_logger.LogDebug("IUrlBuilder returned an error, setting pipeline error");
SetPipelineError(dsUrl.Errors);
return;
}
_logger.LogDebug("downstream url is {downstreamUrl.Data.Value}", dsUrl.Data.Value);
SetDownstreamUrlForThisRequest(dsUrl.Data.Value);
_logger.LogDebug("calling next middleware"); _logger.LogDebug("calling next middleware");

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using Ocelot.Errors;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator
{
public class UrlBuilder : IUrlBuilder
{
public Response<DownstreamUrl> Build(string downstreamPath, string downstreamScheme, HostAndPort downstreamHostAndPort)
{
if (string.IsNullOrEmpty(downstreamPath))
{
return new ErrorResponse<DownstreamUrl>(new List<Error> {new DownstreamPathNullOrEmptyError()});
}
if (string.IsNullOrEmpty(downstreamScheme))
{
return new ErrorResponse<DownstreamUrl>(new List<Error> { new DownstreamSchemeNullOrEmptyError() });
}
if (string.IsNullOrEmpty(downstreamHostAndPort.DownstreamHost))
{
return new ErrorResponse<DownstreamUrl>(new List<Error> { new DownstreamHostNullOrEmptyError() });
}
var builder = new UriBuilder
{
Host = downstreamHostAndPort.DownstreamHost,
Path = downstreamPath,
Scheme = downstreamScheme
};
if (downstreamHostAndPort.DownstreamPort > 0)
{
builder.Port = downstreamHostAndPort.DownstreamPort;
}
var url = builder.Uri.ToString();
return new OkResponse<DownstreamUrl>(new DownstreamUrl(url));
}
}
}

View File

@ -1,25 +1,25 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Ocelot.DownstreamRouteFinder;
using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.Responses; using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer
{ {
public class DownstreamUrlPathPlaceholderReplacer : IDownstreamUrlPathPlaceholderReplacer public class DownstreamTemplatePathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer
{ {
public Response<DownstreamUrl> Replace(string downstreamTemplate, List<UrlPathPlaceholderNameAndValue> urlPathPlaceholderNameAndValues) public Response<DownstreamPath> Replace(PathTemplate downstreamPathTemplate, List<UrlPathPlaceholderNameAndValue> urlPathPlaceholderNameAndValues)
{ {
var upstreamUrl = new StringBuilder(); var downstreamPath = new StringBuilder();
upstreamUrl.Append(downstreamTemplate); downstreamPath.Append(downstreamPathTemplate.Value);
foreach (var placeholderVariableAndValue in urlPathPlaceholderNameAndValues) foreach (var placeholderVariableAndValue in urlPathPlaceholderNameAndValues)
{ {
upstreamUrl.Replace(placeholderVariableAndValue.TemplateVariableName, placeholderVariableAndValue.TemplateVariableValue); downstreamPath.Replace(placeholderVariableAndValue.TemplateVariableName, placeholderVariableAndValue.TemplateVariableValue);
} }
return new OkResponse<DownstreamUrl>(new DownstreamUrl(upstreamUrl.ToString())); return new OkResponse<DownstreamPath>(new DownstreamPath(downstreamPath.ToString()));
} }
} }
} }

View File

@ -1,11 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.Responses; using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer
{ {
public interface IDownstreamUrlPathPlaceholderReplacer public interface IDownstreamPathPlaceholderReplacer
{ {
Response<DownstreamUrl> Replace(string downstreamTemplate, List<UrlPathPlaceholderNameAndValue> urlPathPlaceholderNameAndValues); Response<DownstreamPath> Replace(PathTemplate downstreamPathTemplate, List<UrlPathPlaceholderNameAndValue> urlPathPlaceholderNameAndValues);
} }
} }

View File

@ -1,8 +1,6 @@
using System; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging; using Ocelot.Logging;
@ -41,13 +39,13 @@ namespace Ocelot.Errors.Middleware
var message = CreateMessage(context, e); var message = CreateMessage(context, e);
_logger.LogError(message, e); _logger.LogError(message, e);
await SetInternalServerErrorOnResponse(context); SetInternalServerErrorOnResponse(context);
} }
_logger.LogDebug("ocelot pipeline finished"); _logger.LogDebug("ocelot pipeline finished");
} }
private async Task SetInternalServerErrorOnResponse(HttpContext context) private void SetInternalServerErrorOnResponse(HttpContext context)
{ {
context.Response.OnStarting(x => context.Response.OnStarting(x =>
{ {

View File

@ -4,7 +4,7 @@
{ {
UnauthenticatedError, UnauthenticatedError,
UnknownError, UnknownError,
DownstreamTemplateAlreadyUsedError, DownstreampathTemplateAlreadyUsedError,
UnableToFindDownstreamRouteError, UnableToFindDownstreamRouteError,
CannotAddDataError, CannotAddDataError,
CannotFindDataError, CannotFindDataError,
@ -17,6 +17,16 @@
InstructionNotForClaimsError, InstructionNotForClaimsError,
UnauthorizedError, UnauthorizedError,
ClaimValueNotAuthorisedError, ClaimValueNotAuthorisedError,
UserDoesNotHaveClaimError UserDoesNotHaveClaimError,
DownstreamPathTemplateContainsSchemeError,
DownstreamPathNullOrEmptyError,
DownstreamSchemeNullOrEmptyError,
DownstreamHostNullOrEmptyError,
ServicesAreNullError,
ServicesAreEmptyError,
UnableToFindServiceDiscoveryProviderError,
UnableToFindLoadBalancerError,
RequestTimedOutError,
UnableToFindQoSProviderError
} }
} }

View File

@ -1,7 +1,6 @@
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Ocelot.Infrastructure.RequestData; using Ocelot.Infrastructure.RequestData;
using Ocelot.Logging; using Ocelot.Logging;
using Ocelot.Middleware; using Ocelot.Middleware;

View File

@ -3,8 +3,6 @@ using Ocelot.Responses;
namespace Ocelot.Headers namespace Ocelot.Headers
{ {
using System;
public class RemoveOutputHeaders : IRemoveOutputHeaders public class RemoveOutputHeaders : IRemoveOutputHeaders
{ {
/// <summary> /// <summary>

View File

@ -0,0 +1,24 @@
using System;
namespace Ocelot.Infrastructure.Extensions
{
public static class StringExtensions
{
public static string TrimStart(this string source, string trim, StringComparison stringComparison = StringComparison.Ordinal)
{
if (source == null)
{
return null;
}
string s = source;
while (s.StartsWith(trim, stringComparison))
{
s = s.Substring(trim.Length);
}
return s;
}
}
}

View File

@ -0,0 +1,12 @@
using System.Threading.Tasks;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public interface ILoadBalancer
{
Task<Response<HostAndPort>> Lease();
void Release(HostAndPort hostAndPort);
}
}

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
using Ocelot.Configuration;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public interface ILoadBalancerFactory
{
Task<ILoadBalancer> Get(ReRoute reRoute);
}
}

View File

@ -0,0 +1,10 @@
using Ocelot.Responses;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public interface ILoadBalancerHouse
{
Response<ILoadBalancer> Get(string key);
Response Add(string key, ILoadBalancer loadBalancer);
}
}

View File

@ -0,0 +1,15 @@
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class Lease
{
public Lease(HostAndPort hostAndPort, int connections)
{
HostAndPort = hostAndPort;
Connections = connections;
}
public HostAndPort HostAndPort { get; private set; }
public int Connections { get; private set; }
}
}

View File

@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Ocelot.Errors;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class LeastConnectionLoadBalancer : ILoadBalancer
{
private readonly Func<Task<List<Service>>> _services;
private readonly List<Lease> _leases;
private readonly string _serviceName;
private static readonly object _syncLock = new object();
public LeastConnectionLoadBalancer(Func<Task<List<Service>>> services, string serviceName)
{
_services = services;
_serviceName = serviceName;
_leases = new List<Lease>();
}
public async Task<Response<HostAndPort>> Lease()
{
var services = await _services.Invoke();
if (services == null)
{
return new ErrorResponse<HostAndPort>(new List<Error>() { new ServicesAreNullError($"services were null for {_serviceName}") });
}
if (!services.Any())
{
return new ErrorResponse<HostAndPort>(new List<Error>() { new ServicesAreEmptyError($"services were empty for {_serviceName}") });
}
lock(_syncLock)
{
//todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something?
UpdateServices(services);
var leaseWithLeastConnections = GetLeaseWithLeastConnections();
_leases.Remove(leaseWithLeastConnections);
leaseWithLeastConnections = AddConnection(leaseWithLeastConnections);
_leases.Add(leaseWithLeastConnections);
return new OkResponse<HostAndPort>(new HostAndPort(leaseWithLeastConnections.HostAndPort.DownstreamHost, leaseWithLeastConnections.HostAndPort.DownstreamPort));
}
}
public void Release(HostAndPort hostAndPort)
{
lock(_syncLock)
{
var matchingLease = _leases.FirstOrDefault(l => l.HostAndPort.DownstreamHost == hostAndPort.DownstreamHost
&& l.HostAndPort.DownstreamPort == hostAndPort.DownstreamPort);
if (matchingLease != null)
{
var replacementLease = new Lease(hostAndPort, matchingLease.Connections - 1);
_leases.Remove(matchingLease);
_leases.Add(replacementLease);
}
}
}
private Lease AddConnection(Lease lease)
{
return new Lease(lease.HostAndPort, lease.Connections + 1);
}
private Lease GetLeaseWithLeastConnections()
{
//now get the service with the least connections?
Lease leaseWithLeastConnections = null;
for (var i = 0; i < _leases.Count; i++)
{
if (i == 0)
{
leaseWithLeastConnections = _leases[i];
}
else
{
if (_leases[i].Connections < leaseWithLeastConnections.Connections)
{
leaseWithLeastConnections = _leases[i];
}
}
}
return leaseWithLeastConnections;
}
private Response UpdateServices(List<Service> services)
{
if (_leases.Count > 0)
{
var leasesToRemove = new List<Lease>();
foreach (var lease in _leases)
{
var match = services.FirstOrDefault(s => s.HostAndPort.DownstreamHost == lease.HostAndPort.DownstreamHost
&& s.HostAndPort.DownstreamPort == lease.HostAndPort.DownstreamPort);
if (match == null)
{
leasesToRemove.Add(lease);
}
}
foreach (var lease in leasesToRemove)
{
_leases.Remove(lease);
}
foreach (var service in services)
{
var exists = _leases.FirstOrDefault(l => l.HostAndPort.ToString() == service.HostAndPort.ToString());
if (exists == null)
{
_leases.Add(new Lease(service.HostAndPort, 0));
}
}
}
else
{
foreach (var service in services)
{
_leases.Add(new Lease(service.HostAndPort, 0));
}
}
return new OkResponse();
}
}
}

View File

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using Ocelot.Configuration;
using Ocelot.ServiceDiscovery;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class LoadBalancerFactory : ILoadBalancerFactory
{
private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory;
public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory)
{
_serviceProviderFactory = serviceProviderFactory;
}
public async Task<ILoadBalancer> Get(ReRoute reRoute)
{
var serviceProvider = _serviceProviderFactory.Get(reRoute.ServiceProviderConfiguraion);
switch (reRoute.LoadBalancer)
{
case "RoundRobin":
return new RoundRobinLoadBalancer(await serviceProvider.Get());
case "LeastConnection":
return new LeastConnectionLoadBalancer(async () => await serviceProvider.Get(), reRoute.ServiceProviderConfiguraion.ServiceName);
default:
return new NoLoadBalancer(await serviceProvider.Get());
}
}
}
}

View File

@ -0,0 +1,42 @@
using System.Collections.Generic;
using Ocelot.Responses;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class LoadBalancerHouse : ILoadBalancerHouse
{
private readonly Dictionary<string, ILoadBalancer> _loadBalancers;
public LoadBalancerHouse()
{
_loadBalancers = new Dictionary<string, ILoadBalancer>();
}
public Response<ILoadBalancer> Get(string key)
{
ILoadBalancer loadBalancer;
if(_loadBalancers.TryGetValue(key, out loadBalancer))
{
return new OkResponse<ILoadBalancer>(_loadBalancers[key]);
}
return new ErrorResponse<ILoadBalancer>(new List<Ocelot.Errors.Error>()
{
new UnableToFindLoadBalancerError($"unabe to find load balancer for {key}")
});
}
public Response Add(string key, ILoadBalancer loadBalancer)
{
if (!_loadBalancers.ContainsKey(key))
{
_loadBalancers.Add(key, loadBalancer);
}
_loadBalancers.Remove(key);
_loadBalancers.Add(key, loadBalancer);
return new OkResponse();
}
}
}

View File

@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class NoLoadBalancer : ILoadBalancer
{
private readonly List<Service> _services;
public NoLoadBalancer(List<Service> services)
{
_services = services;
}
public async Task<Response<HostAndPort>> Lease()
{
var service = await Task.FromResult(_services.FirstOrDefault());
return new OkResponse<HostAndPort>(service.HostAndPort);
}
public void Release(HostAndPort hostAndPort)
{
}
}
}

View File

@ -0,0 +1,34 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Ocelot.Responses;
using Ocelot.Values;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class RoundRobinLoadBalancer : ILoadBalancer
{
private readonly List<Service> _services;
private int _last;
public RoundRobinLoadBalancer(List<Service> services)
{
_services = services;
}
public async Task<Response<HostAndPort>> Lease()
{
if (_last >= _services.Count)
{
_last = 0;
}
var next = await Task.FromResult(_services[_last]);
_last++;
return new OkResponse<HostAndPort>(next.HostAndPort);
}
public void Release(HostAndPort hostAndPort)
{
}
}
}

View File

@ -0,0 +1,12 @@
using Ocelot.Errors;
namespace Ocelot.LoadBalancer.LoadBalancers
{
public class ServicesAreEmptyError : Error
{
public ServicesAreEmptyError(string message)
: base(message, OcelotErrorCode.ServicesAreEmptyError)
{
}
}
}

Some files were not shown because too many files have changed in this diff Show More