diff --git a/src/Ocelot.Library/.gitignore b/.gitignore similarity index 98% rename from src/Ocelot.Library/.gitignore rename to .gitignore index 0ca27f04..1a7759c2 100644 --- a/src/Ocelot.Library/.gitignore +++ b/.gitignore @@ -21,11 +21,14 @@ build/ bld/ [Bb]in/ [Oo]bj/ +results/ # Visual Studio 2015 cache/options directory .vs/ +.vscode/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ +site/wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ @@ -232,3 +235,4 @@ _Pvt_Extensions # FAKE - F# Make .fake/ +tools/ diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 00000000..05e9ac41 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,4 @@ +mode: ContinuousDelivery +branches: {} +ignore: + sha: [] diff --git a/Ocelot.nuspec b/Ocelot.nuspec new file mode 100644 index 00000000..d236d60d --- /dev/null +++ b/Ocelot.nuspec @@ -0,0 +1,48 @@ + + + + Ocelot + 1.0.0 + Tom Pallister + Tom Pallister + https://github.com/TomPallister/Ocelot/blob/develop/LICENSE.md + https://github.com/TomPallister/Ocelot + false + Ocelot Api Gateway + Latest Ocelot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ocelot.sln b/Ocelot.sln new file mode 100644 index 00000000..2f8fd206 --- /dev/null +++ b/Ocelot.sln @@ -0,0 +1,76 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}" + ProjectSection(SolutionItems) = preProject + .gitignore = .gitignore + appveyor.yml = appveyor.yml + build-and-run-tests.ps1 = build-and-run-tests.ps1 + build.cake = build.cake + build.ps1 = build.ps1 + configuration-explanation.txt = configuration-explanation.txt + global.json = global.json + LICENSE.md = LICENSE.md + Ocelot.nuspec = Ocelot.nuspec + README.md = README.md + release.ps1 = release.ps1 + run-acceptance-tests.ps1 = run-acceptance-tests.ps1 + run-benchmarks.bat = run-benchmarks.bat + run-benchmarks.ps1 = run-benchmarks.ps1 + run-unit-tests.ps1 = run-unit-tests.ps1 + EndProjectSection +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot", "src\Ocelot\Ocelot.xproj", "{D6DF4206-0DBA-41D8-884D-C3E08290FDBB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5B401523-36DA-4491-B73A-7590A26E420B}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.UnitTests", "test\Ocelot.UnitTests\Ocelot.UnitTests.xproj", "{54E84F1A-E525-4443-96EC-039CBD50C263}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.AcceptanceTests", "test\Ocelot.AcceptanceTests\Ocelot.AcceptanceTests.xproj", "{F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.ManualTest", "test\Ocelot.ManualTest\Ocelot.ManualTest.xproj", "{02BBF4C5-517E-4157-8D21-4B8B9E118B7A}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Ocelot.Benchmarks", "test\Ocelot.Benchmarks\Ocelot.Benchmarks.xproj", "{106B49E6-95F6-4A7B-B81C-96BFA74AF035}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB}.Release|Any CPU.Build.0 = Release|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Debug|Any CPU.Build.0 = Debug|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.ActiveCfg = Release|Any CPU + {54E84F1A-E525-4443-96EC-039CBD50C263}.Release|Any CPU.Build.0 = Release|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8C224FE-36BE-45F5-9B0E-666D8F4A9B52}.Release|Any CPU.Build.0 = Release|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {02BBF4C5-517E-4157-8D21-4B8B9E118B7A}.Release|Any CPU.Build.0 = Release|Any CPU + {106B49E6-95F6-4A7B-B81C-96BFA74AF035}.Debug|Any CPU.ActiveCfg = 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.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D6DF4206-0DBA-41D8-884D-C3E08290FDBB} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} + {54E84F1A-E525-4443-96EC-039CBD50C263} = {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} + {106B49E6-95F6-4A7B-B81C-96BFA74AF035} = {5B401523-36DA-4491-B73A-7590A26E420B} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index 85884775..249a99a3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,391 @@ # Ocelot +[![Build status](https://ci.appveyor.com/api/projects/status/roahbe4nl526ysya?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot) + +[![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) + 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. + +This is not ready for production yet as uses a lot of rc and beta .net core packages. +Hopefully by the start of 2017 it will be in use. + +## Contributing + +Pull requests, issues and commentary welcome! No special process just create a request and get in +touch either via gitter or create an issue. + +## How to install + +Ocelot is designed to work with ASP.NET core only and is currently +built to netcoreapp1.4 [this](https://docs.microsoft.com/en-us/dotnet/articles/standard/library) documentation may prove helpful when working out if Ocelot would be suitable for you. + +Install Ocelot and it's dependecies using nuget. At the moment +all we have is the pre version. Once we have something working in +a half decent way we will drop a version. + +`Install-Package Ocelot -Pre` + +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. + + { + "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. + + public class Startup + { + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddJsonFile("configuration.json") + .AddEnvironmentVariables(); + + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + Action settings = (x) => + { + x.WithMicrosoftLogging(log => + { + 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(); + } + } + + +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 UpstreamTemplate. + +Ocelot's describes the routing of one request to another as a ReRoute. In order to get +anything working in Ocelot you need to set up a ReRoute in the configuration. + + { + "ReRoutes": [ + ] + } + +In order to set up a ReRoute you need to add one to the json array called ReRoutes like +the following. + + { + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "DownstreamPort": 80, + "DownstreamHost" "localhost" + "UpstreamTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Put" + } + +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 +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 UpstreamTemplate. If it is +Ocelot will attempt to replace the placeholder with the correct variable value from the +Upstream URL when the request comes in. + +At the moment without any configuration Ocelot will default to all ReRoutes being case insensitive. +In order to change this you can specify on a per ReRoute basis the following setting. + + "ReRouteIsCaseSensitive": true + +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! + +## Authentication + +Ocelot currently supports the use of bearer tokens with Identity Server (more providers to +come if required). In order to identity a ReRoute as authenticated it needs the following +configuration added. + + "AuthenticationOptions": { + "Provider": "IdentityServer", + "ProviderRootUrl": "http://localhost:52888", + "ScopeName": "api", + "AdditionalScopes": [ + "openid", + "offline_access" + ], + "ScopeSecret": "secret" + } + +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. + + "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 + + "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 + + "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 + + "AddQueriesToRequest": { + "LocationId": "Claims[LocationId] > value", + }, + +This shows a transform where Ocelot looks at the users LocationId claim and add its as +a query string parameter to be forwarded onto the downstream service. + +## Logging + +Ocelot uses the standard logging interfaces ILoggerFactory / ILogger at the moment. +This is encapsulated in IOcelotLogger / IOcelotLoggerFactory with an implementation +for the standard asp.net core logging stuff at the moment. + +There are a bunch of debugging logs in the ocelot middlewares however I think the +system probably needs more logging in the code it calls into. Other than the debugging +there is a global error handler that should catch any errors thrown and log them as errors. + +The reason for not just using bog standard framework logging is that I could not +work out how to override the request id that get's logged when setting IncludeScopes +to true for logging settings. Nicely onto the next feature. + +## 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 + + "RequestIdKey": "OcRequestId" + +In this example OcRequestId is the request header that contains the clients request id. + +There is also a setting in the GlobalConfiguration section which will override whatever has been +set at ReRoute level for the request id. The setting is as fllows. + + "RequestIdKey": "OcRequestId", + +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. + + "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. + + 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. + +## Not supported + +Ocelot does not support... + ++ Chunked Encoding - Ocelot will always get the body size and return Content-Length +header. Sorry if this doesn't work for your use case! + ++ Fowarding a host header - The host header that you send to Ocelot will not be +forwarded to the downstream service. Obviously this would break everything :( + +## Things that are currently annoying me + ++ The ReRoute configuration object is too large. + ++ The base OcelotMiddleware lets you access things that are going to be null +and doesnt check the response is OK. I think the fact you can even call stuff +that isnt available is annoying. Let alone it be null. + +## Coming up + +You can see what we are working on [here](https://github.com/TomPallister/Ocelot/projects/1) + + diff --git a/ReleaseNotes.md b/ReleaseNotes.md new file mode 100644 index 00000000..a647c6c3 --- /dev/null +++ b/ReleaseNotes.md @@ -0,0 +1 @@ +No issues closed since last release \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..b5cd7c0c --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,8 @@ +version: 1.0.{build} +configuration: +- Release +platform: Any CPU +build_script: +- ./build.ps1 +cache: +- '%USERPROFILE%\.nuget\packages' \ No newline at end of file diff --git a/build-and-release-unstable.ps1 b/build-and-release-unstable.ps1 new file mode 100644 index 00000000..51c6f0d5 --- /dev/null +++ b/build-and-release-unstable.ps1 @@ -0,0 +1 @@ +./build.ps1 -target BuildAndReleaseUnstable \ No newline at end of file diff --git a/build-and-run-tests.ps1 b/build-and-run-tests.ps1 new file mode 100644 index 00000000..f82502e5 --- /dev/null +++ b/build-and-run-tests.ps1 @@ -0,0 +1 @@ +./build.ps1 -target RunTests \ No newline at end of file diff --git a/build.cake b/build.cake new file mode 100644 index 00000000..1d798d74 --- /dev/null +++ b/build.cake @@ -0,0 +1,347 @@ +#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"); +var acceptanceTestAssemblies = @"./test/Ocelot.AcceptanceTests"; + +// 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 = ""; +var buildVersion = committedVersion; +var committedVersion = "0.0.0-dev"; + +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(() => + { + var nugetVersion = GetNuGetVersionForCommit(); + Information("SemVer version number: " + nugetVersion); + + if (AppVeyor.IsRunningOnAppVeyor) + { + Information("Persisting version number..."); + PersistVersion(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("RunBenchmarkTests") + .IsDependentOn("Restore") + .Does(() => + { + var buildSettings = new DotNetCoreRunSettings + { + Configuration = compileConfig, + }; + + EnsureDirectoryExists(artifactsForBenchmarkTestsDir); + + DoInDirectory(benchmarkTestAssemblies, () => + { + DotNetCoreRun(".", "", buildSettings); + }); + }); + +Task("RunTests") + .IsDependentOn("RunUnitTests") + .IsDependentOn("RunAcceptanceTests") + .Does(() => + { + }); + +Task("CreatePackages") + .Does(() => + { + EnsureDirectoryExists(packagesDir); + + GenerateReleaseNotes(); + + 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(() => + { + PublishPackages(nugetFeedUnstableKey, nugetFeedUnstableUploadUrl, nugetFeedUnstableSymbolsUploadUrl); + }); + +Task("EnsureStableReleaseRequirements") + .Does(() => + { + if (!AppVeyor.IsRunningOnAppVeyor) + { + throw new Exception("Stable release should happen via appveyor"); + } + + var isTag = + AppVeyor.Environment.Repository.Tag.IsTag && + !string.IsNullOrWhiteSpace(AppVeyor.Environment.Repository.Tag.Name); + + if (!isTag) + { + throw new Exception("Stable release should happen from a published GitHub release"); + } + }); + +Task("UpdateVersionInfo") + .IsDependentOn("EnsureStableReleaseRequirements") + .Does(() => + { + releaseTag = AppVeyor.Environment.Repository.Tag.Name; + AppVeyor.UpdateBuildVersion(releaseTag); + }); + +Task("DownloadGitHubReleaseArtifacts") + .IsDependentOn("UpdateVersionInfo") + .Does(() => + { + EnsureDirectoryExists(packagesDir); + + var releaseUrl = tagsUrl + releaseTag; + var assets_url = ParseJson(GetResource(releaseUrl)) + .GetValue("assets_url") + .Value(); + + foreach(var asset in DeserializeJson(GetResource(assets_url))) + { + var file = packagesDir + File(asset.Value("name")); + Information("Downloading " + file); + DownloadFile(asset.Value("browser_download_url"), file); + } + }); + +Task("ReleasePackagesToStableFeed") + .IsDependentOn("DownloadGitHubReleaseArtifacts") + .Does(() => + { + PublishPackages(nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); + }); + +Task("Release") + .IsDependentOn("ReleasePackagesToStableFeed"); + +RunTarget(target); + +/// Gets nuique nuget version for this commit +private string GetNuGetVersionForCommit() +{ + GitVersion(new GitVersionSettings{ + UpdateAssemblyInfo = false, + OutputType = GitVersionOutput.BuildServer + }); + + var versionInfo = GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json }); + return versionInfo.NuGetVersion; +} + +/// Updates project version in all of our projects +private void PersistVersion(string version) +{ + Information(string.Format("We'll search all project.json files for {0} and replace with {1}...", committedVersion, version)); + + 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, version); + + System.IO.File.WriteAllText(file, updatedProjectJson); + } +} + +/// generates release notes based on issues closed in GitHub since the last release +private void GenerateReleaseNotes() +{ + Information("Generating release notes at " + releaseNotesFile); + + var releaseNotesExitCode = StartProcess( + @"tools/GitReleaseNotes/tools/gitreleasenotes.exe", + new ProcessSettings { Arguments = ". /o " + releaseNotesFile }); + + if (string.IsNullOrEmpty(System.IO.File.ReadAllText(releaseNotesFile))) + { + System.IO.File.WriteAllText(releaseNotesFile, "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(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"]); + var symbolsPackage = packagesDir + File(artifacts["nugetSymbols"]); + + NuGetPush( + codePackage, + new NuGetPushSettings { + ApiKey = feedApiKey, + Source = codeFeedUrl + }); + + NuGetPush( + symbolsPackage, + new NuGetPushSettings { + ApiKey = feedApiKey, + Source = symbolFeedUrl + }); + +} + +/// 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(); + } +} \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 00000000..44de5793 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,189 @@ +########################################################################## +# This is the Cake bootstrapper script for PowerShell. +# This file was downloaded from https://github.com/cake-build/resources +# Feel free to change this file to fit your needs. +########################################################################## + +<# + +.SYNOPSIS +This is a Powershell script to bootstrap a Cake build. + +.DESCRIPTION +This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) +and execute your Cake build script with the parameters you provide. + +.PARAMETER Script +The build script to execute. +.PARAMETER Target +The build script target to run. +.PARAMETER Configuration +The build configuration to use. +.PARAMETER Verbosity +Specifies the amount of information to be displayed. +.PARAMETER Experimental +Tells Cake to use the latest Roslyn release. +.PARAMETER WhatIf +Performs a dry run of the build script. +No tasks will be executed. +.PARAMETER Mono +Tells Cake to use the Mono scripting engine. +.PARAMETER SkipToolPackageRestore +Skips restoring of packages. +.PARAMETER ScriptArgs +Remaining arguments are added here. + +.LINK +http://cakebuild.net + +#> + +[CmdletBinding()] +Param( + [string]$Script = "build.cake", + [string]$Target = "Default", + [ValidateSet("Release", "Debug")] + [string]$Configuration = "Release", + [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] + [string]$Verbosity = "Verbose", + [switch]$Experimental, + [Alias("DryRun","Noop")] + [switch]$WhatIf, + [switch]$Mono, + [switch]$SkipToolPackageRestore, + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$ScriptArgs +) + +[Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null +function MD5HashFile([string] $filePath) +{ + if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) + { + return $null + } + + [System.IO.Stream] $file = $null; + [System.Security.Cryptography.MD5] $md5 = $null; + try + { + $md5 = [System.Security.Cryptography.MD5]::Create() + $file = [System.IO.File]::OpenRead($filePath) + return [System.BitConverter]::ToString($md5.ComputeHash($file)) + } + finally + { + if ($file -ne $null) + { + $file.Dispose() + } + } +} + +Write-Host "Preparing to run build script..." + +if(!$PSScriptRoot){ + $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent +} + +$TOOLS_DIR = Join-Path $PSScriptRoot "tools" +$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" +$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" +$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" +$PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" +$PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" + +# Should we use mono? +$UseMono = ""; +if($Mono.IsPresent) { + Write-Verbose -Message "Using the Mono based scripting engine." + $UseMono = "-mono" +} + +# Should we use the new Roslyn? +$UseExperimental = ""; +if($Experimental.IsPresent -and !($Mono.IsPresent)) { + Write-Verbose -Message "Using experimental version of Roslyn." + $UseExperimental = "-experimental" +} + +# Is this a dry run? +$UseDryRun = ""; +if($WhatIf.IsPresent) { + $UseDryRun = "-dryrun" +} + +# Make sure tools folder exists +if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { + Write-Verbose -Message "Creating tools directory..." + New-Item -Path $TOOLS_DIR -Type directory | out-null +} + +# Make sure that packages.config exist. +if (!(Test-Path $PACKAGES_CONFIG)) { + Write-Verbose -Message "Downloading packages.config..." + try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { + Throw "Could not download packages.config." + } +} + +# Try find NuGet.exe in path if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Trying to find nuget.exe in PATH..." + $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } + $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 + if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { + Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." + $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName + } +} + +# Try download NuGet.exe if not exists +if (!(Test-Path $NUGET_EXE)) { + Write-Verbose -Message "Downloading NuGet.exe..." + try { + (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) + } catch { + Throw "Could not download NuGet.exe." + } +} + +# Save nuget.exe path to environment to be available to child processed +$ENV:NUGET_EXE = $NUGET_EXE + +# Restore tools from NuGet? +if(-Not $SkipToolPackageRestore.IsPresent) { + Push-Location + Set-Location $TOOLS_DIR + + # Check for changes in packages.config and remove installed tools if true. + [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) + if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or + ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { + Write-Verbose -Message "Missing or changed package.config hash..." + Remove-Item * -Recurse -Exclude packages.config,nuget.exe + } + + Write-Verbose -Message "Restoring tools from NuGet..." + $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" + + if ($LASTEXITCODE -ne 0) { + Throw "An error occured while restoring NuGet tools." + } + else + { + $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" + } + Write-Verbose -Message ($NuGetOutput | out-string) + Pop-Location +} + +# Make sure that Cake has been installed. +if (!(Test-Path $CAKE_EXE)) { + Throw "Could not find Cake.exe at $CAKE_EXE" +} + +# Start Cake +Write-Host "Running build script..." +Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" +exit $LASTEXITCODE \ No newline at end of file diff --git a/build.readme.md b/build.readme.md new file mode 100644 index 00000000..fccff4d5 --- /dev/null +++ b/build.readme.md @@ -0,0 +1,22 @@ +#1. Overview + +This document summarises the build and release process for the project. The build scripts are written using [Cake](http://cakebuild.net/), and are defined in `./build.cake`. The scripts have been designed to be run by either developers locally or by a build server (currently [AppVeyor](https://www.appveyor.com/)), with minimal logic defined in the build server itself. + +#2. Building + * You'll generally want to run the `./build.ps1` script. This will compile, run unit and acceptance tests and build the output packages locally. Output will got to the `./artifacts` directory. + * You can view the current commit's [SemVer](http://semver.org/) build information by running `./version.ps1`. + * The other `./*.ps1` scripts perform subsets of the build process, if you don't want to run the full build. + * The release process works best with GitFlow branching; this allows us to publish every development commit to an unstable feed with a unique SemVer version, and then choose when to release to a stable feed. + +#3. Release process +This section defines the release process for the maintainers of the project. + * Merge pull requests to the `release` branch. + * Every commit pushed to the Origin repo will kick off the [ocelot-build](https://ci.appveyor.com/project/binarymash/ocelot) project in AppVeyor. This performs the same tasks as the command line build, and in addition pushes the packages to the unstable nuget feed. + * When you're ready for a release, create a release branch. You'll probably want to update the committed `./ReleaseNotes.md` based on the contents of the equivalent file in the `./artifacts` directory. + * When the `release` branch has built successfully in Appveyor, select the build and then Deploy to the `GitHub Release` environment. This will create a new release in GitHub. + * In Github, navigate to the [release](https://github.com/binarymash/Ocelot/releases). Modify the release name and tag as desired. + * When you're ready, publish the release. This will tag the commit with the specified release number. + * The [ocelot-release](https://ci.appveyor.com/project/binarymash/ocelot-wtaj9) project will detect the newly created tag and kick off the release process. This will download the artifacts from GitHub, and publish the packages to the stable nuget feed. + * When you have a final stable release build, merge the `release` branch into `master` and `develop`. Deploy the master branch to github and following the full release process as described above. Don't forget to uncheck the "This is a pre-release" checkbox in GitHub before publishing. + * Note - because the release builds are initiated by tagging a commit, if for some reason a release build fails in AppVeyor you'll need to delete the tag from the repo and republish the release in GitHub. + diff --git a/configuration-explanation.txt b/configuration-explanation.txt new file mode 100644 index 00000000..ad020469 --- /dev/null +++ b/configuration-explanation.txt @@ -0,0 +1,91 @@ +{ + "ReRoutes": [ + { + # The downstream path we are forwarding the request to, ocelot will not add a trailing slash. + # Ocelot replaces any placeholders {etc} with matched values from the incoming request. + "DownstreamPathTemplate": "/identityserverexample/{someid}/something", + # The scheme you want Ocelot to use when making the downstream request + "DownstreamScheme": "https", + # The port you want Ocelot to use when making the downstream request, will default to + # scheme if nothing set + "DownstreamPort": 80, + # The host address of the downstream service, should not have a trailing slash or scheme + # if there is a trailing slash Ocelot will remove it. + "DownstreamHost" "localhost" + # The path template we are listening on for this re route, Ocelot will add a trailing + # slash to this property. Then when a request is made Ocelot makes sure a trailing + # slash is added, so everything matches + "UpstreamTemplate": "/identityserverexample", + # The method we are listening for on this re route + "UpstreamHttpMethod": "Get", + # Only support identity server at the moment + "AuthenticationOptions": { + "Provider": "IdentityServer", + "ProviderRootUrl": "http://localhost:52888", + "ScopeName": "api", + "AdditionalScopes": [ + "openid", + "offline_access" + ], + # Required if using reference tokens + "ScopeSecret": "secret" + }, + # WARNING - will overwrite any headers already in the request with these values. + # Ocelot will look in the user claims for the key in [] then return the value and save + # it as a header with the given key before the colon (:). The index selection on value + # means that Ocelot will use the delimiter specified after the next > to split the + # claim value and return the index specified. + "AddHeadersToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + # WARNING - will overwrite any claims already in the request with these values. + # Ocelot will look in the user claims for the key in [] then return the value and save + # it as a claim with the given key before the colon (:). The index selection on value + # means that Ocelot will use the delimiter specified after the next > to split the + # claim value and return the index specified. + "AddClaimsToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + # WARNING - will overwrite any query string entries already in the request with these values. + # Ocelot will look in the user claims for the key in [] then return the value and save + # it as a query string with the given key before the colon (:). The index selection on value + # means that Ocelot will use the delimiter specified after the next > to split the + # claim value and return the index specified. + "AddQueriesToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + # This specifies any claims that are required for the user to access this re route. + # In this example the user must have the claim type UserType and + # the value must be registered + "RouteClaimsRequirement": { + "UserType": "registered" + }, + # This tells Ocelot to look for a header and use its value as a request/correlation id. + # If it is set here then the id will be forwarded to the downstream service. If it + # does not then it will not be forwarded + "RequestIdKey": "OcRequestId", + # If this is set the response from the downstream service will be cached using the key that called it. + # This gives the user a chance to influence the key by adding some random query string paramter for + # a user id or something that would get ignored by the downstream service. This is a hack and I + # intend to provide a mechanism the user can specify for the ttl caching. Also want to expand + # the caching a lot. + "FileCacheOptions": { "TtlSeconds": 15 }, + # The value of this is used when matching the upstream template to an upstream url. + "ReRouteIsCaseSensitive": false + }, + # This section is meant to be for global configuration settings + "GlobalConfiguration": { + # If this is set it will override any route specific request id keys, behaves the same + # otherwise + "RequestIdKey": "OcRequestId", + } + } \ No newline at end of file diff --git a/configuration.yaml b/configuration.yaml new file mode 100755 index 00000000..2e47e77d --- /dev/null +++ b/configuration.yaml @@ -0,0 +1,3 @@ +Routes: +- Downstream: http://localhost:51879/ + Upstream: /heee diff --git a/global.json b/global.json new file mode 100644 index 00000000..ff8d898e --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ + { + "projects": [ "src", "test" ], + "sdk": { + "version": "1.0.0-preview2-003133" + } +} diff --git a/release.ps1 b/release.ps1 new file mode 100644 index 00000000..6cf4c66b --- /dev/null +++ b/release.ps1 @@ -0,0 +1 @@ +./build.ps1 -target Release \ No newline at end of file diff --git a/run-acceptance-tests.ps1 b/run-acceptance-tests.ps1 new file mode 100644 index 00000000..480e1d4c --- /dev/null +++ b/run-acceptance-tests.ps1 @@ -0,0 +1 @@ +./build -target RunAcceptanceTests \ No newline at end of file diff --git a/run-benchmarks.ps1 b/run-benchmarks.ps1 new file mode 100644 index 00000000..e05490fd --- /dev/null +++ b/run-benchmarks.ps1 @@ -0,0 +1 @@ +./build.ps1 -target RunBenchmarkTests \ No newline at end of file diff --git a/run-unit-tests.ps1 b/run-unit-tests.ps1 new file mode 100644 index 00000000..0e6a91bd --- /dev/null +++ b/run-unit-tests.ps1 @@ -0,0 +1 @@ +./build.ps1 -target RunUnitTests \ No newline at end of file diff --git a/src/Ocelot.ApiGateway/AddRouteHandler.cs b/src/Ocelot.ApiGateway/AddRouteHandler.cs deleted file mode 100644 index baaa05a4..00000000 --- a/src/Ocelot.ApiGateway/AddRouteHandler.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace Ocelot.ApiGateway -{ - public static class HelloExtensions - { - public static IRouteBuilder AddRouter(this IRouteBuilder routeBuilder, - IApplicationBuilder app) - { - routeBuilder.Routes.Add(new Route(new Router(), - "{*url}", - app.ApplicationServices.GetService())); - - return routeBuilder; - } - } -} \ No newline at end of file diff --git a/src/Ocelot.ApiGateway/Dockerfile b/src/Ocelot.ApiGateway/Dockerfile deleted file mode 100644 index 5ea50452..00000000 --- a/src/Ocelot.ApiGateway/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM microsoft/aspnet:1.0.0-rc1-update1 - -RUN printf "deb http://ftp.us.debian.org/debian jessie main\n" >> /etc/apt/sources.list -RUN apt-get -qq update && apt-get install -qqy sqlite3 libsqlite3-dev && rm -rf /var/lib/apt/lists/* - -COPY . /app -WORKDIR /app -RUN ["dnu", "restore"] - -EXPOSE 5000/tcp -ENTRYPOINT ["dnx", "-p", "project.json", "Microsoft.AspNet.Server.Kestrel", "--server.urls", "http://0.0.0.0:5000"] diff --git a/src/Ocelot.ApiGateway/Routers.cs b/src/Ocelot.ApiGateway/Routers.cs deleted file mode 100644 index af8881bc..00000000 --- a/src/Ocelot.ApiGateway/Routers.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; - -namespace Ocelot.ApiGateway -{ - public class Router : IRouter - { - public Task RouteAsync(RouteContext context) - { - context.Handler = async c => - { - await c.Response.WriteAsync($"Hi, Tom!"); - }; - - return Task.FromResult(0); - } - - public VirtualPathData GetVirtualPath(VirtualPathContext context) - { - return null; - } - } -} \ No newline at end of file diff --git a/src/Ocelot.ApiGateway/project.json b/src/Ocelot.ApiGateway/project.json deleted file mode 100644 index 05f7e266..00000000 --- a/src/Ocelot.ApiGateway/project.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.0.0-rc2-3002702", - "type": "platform" - }, - "Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-rc2-final", - "Microsoft.Extensions.Configuration.FileExtensions": "1.0.0-rc2-final", - "Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-final", - "Microsoft.Extensions.Logging": "1.0.0-rc2-final", - "Microsoft.Extensions.Logging.Console": "1.0.0-rc2-final", - "Microsoft.Extensions.Logging.Debug": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Routing": "1.0.0-rc2-final" - }, - - "tools": { - "Microsoft.AspNetCore.Server.IISIntegration.Tools": { - "version": "1.0.0-preview1-final", - "imports": "portable-net45+win8+dnxcore50" - } - }, - - "frameworks": { - "netcoreapp1.0": { - "imports": [ - "dotnet5.6", - "dnxcore50", - "portable-net45+win8" - ] - } - }, - - "buildOptions": { - "emitEntryPoint": true, - "preserveCompilationContext": true - }, - - "runtimeOptions": { - "gcServer": true - }, - - "publishOptions": { - "include": [ - "wwwroot", - "Views", - "appsettings.json", - "web.config" - ] - }, - - "scripts": { - "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] - }, - - "tooling": { - "defaultNamespace": "Ocelot.ApiGateway" - } -} diff --git a/src/Ocelot.Library/RouterMiddleware.cs b/src/Ocelot.Library/RouterMiddleware.cs deleted file mode 100644 index d4474325..00000000 --- a/src/Ocelot.Library/RouterMiddleware.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Ocelot.Library -{ - // This project can output the Class library as a NuGet Package. - // To enable this option, right-click on the project and select the Properties menu item. In the Build tab select "Produce outputs on build". - public class RouterMiddleware - { - public RouterMiddleware() - { - } - } -} diff --git a/src/Ocelot.Library/project.json b/src/Ocelot.Library/project.json deleted file mode 100644 index b72a5c6a..00000000 --- a/src/Ocelot.Library/project.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "1.0.0-*", - - "dependencies": { - "NETStandard.Library": "1.5.0-rc2-24027" - }, - - "frameworks": { - "netstandard1.5": { - "imports": "dnxcore50" - } - }, - - "tooling": { - "defaultNamespace": "Ocelot.Library" - } -} diff --git a/src/Ocelot/Authentication/Handler/AuthenticationHandler.cs b/src/Ocelot/Authentication/Handler/AuthenticationHandler.cs new file mode 100644 index 00000000..3cb662b8 --- /dev/null +++ b/src/Ocelot/Authentication/Handler/AuthenticationHandler.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Authentication.Handler +{ + public class AuthenticationHandler + { + public AuthenticationHandler(string provider, IHandler handler) + { + Provider = provider; + Handler = handler; + } + + public string Provider { get; private set; } + public IHandler Handler { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Authentication/Handler/Creator/AuthenticationHandlerCreator.cs b/src/Ocelot/Authentication/Handler/Creator/AuthenticationHandlerCreator.cs new file mode 100644 index 00000000..65260d64 --- /dev/null +++ b/src/Ocelot/Authentication/Handler/Creator/AuthenticationHandlerCreator.cs @@ -0,0 +1,34 @@ +using IdentityServer4.AccessTokenValidation; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Ocelot.Responses; + +namespace Ocelot.Authentication.Handler.Creator +{ + using AuthenticationOptions = Configuration.AuthenticationOptions; + + /// + /// Cannot unit test things in this class due to use of extension methods + /// + public class AuthenticationHandlerCreator : IAuthenticationHandlerCreator + { + public Response CreateIdentityServerAuthenticationHandler(IApplicationBuilder app, AuthenticationOptions authOptions) + { + var builder = app.New(); + + builder.UseIdentityServerAuthentication(new IdentityServerAuthenticationOptions + { + Authority = authOptions.ProviderRootUrl, + ApiName = authOptions.ScopeName, + RequireHttpsMetadata = authOptions.RequireHttps, + AllowedScopes = authOptions.AdditionalScopes, + SupportedTokens = SupportedTokens.Both, + ApiSecret = authOptions.ScopeSecret + }); + + var authenticationNext = builder.Build(); + + return new OkResponse(authenticationNext); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Authentication/Handler/Creator/IAuthenticationHandlerCreator.cs b/src/Ocelot/Authentication/Handler/Creator/IAuthenticationHandlerCreator.cs new file mode 100644 index 00000000..6baa0385 --- /dev/null +++ b/src/Ocelot/Authentication/Handler/Creator/IAuthenticationHandlerCreator.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Ocelot.Responses; + +namespace Ocelot.Authentication.Handler.Creator +{ + using AuthenticationOptions = Configuration.AuthenticationOptions; + + public interface IAuthenticationHandlerCreator + { + Response CreateIdentityServerAuthenticationHandler(IApplicationBuilder app, AuthenticationOptions authOptions); + } +} diff --git a/src/Ocelot/Authentication/Handler/Factory/AuthenticationHandlerFactory.cs b/src/Ocelot/Authentication/Handler/Factory/AuthenticationHandlerFactory.cs new file mode 100644 index 00000000..6379cc1f --- /dev/null +++ b/src/Ocelot/Authentication/Handler/Factory/AuthenticationHandlerFactory.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Ocelot.Authentication.Handler.Creator; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.Authentication.Handler.Factory +{ + using AuthenticationOptions = Configuration.AuthenticationOptions; + + public class AuthenticationHandlerFactory : IAuthenticationHandlerFactory + { + private readonly IAuthenticationHandlerCreator _creator; + + public AuthenticationHandlerFactory(IAuthenticationHandlerCreator creator) + { + _creator = creator; + } + + public Response Get(IApplicationBuilder app, AuthenticationOptions authOptions) + { + var handler = _creator.CreateIdentityServerAuthenticationHandler(app, authOptions); + + if (!handler.IsError) + { + return new OkResponse( + new AuthenticationHandler(authOptions.Provider, new RequestDelegateHandler(handler.Data))); + } + + return new ErrorResponse(new List + { + new UnableToCreateAuthenticationHandlerError($"Unable to create authentication handler for {authOptions.Provider}") + }); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Authentication/Handler/Factory/IAuthenticationHandlerFactory.cs b/src/Ocelot/Authentication/Handler/Factory/IAuthenticationHandlerFactory.cs new file mode 100644 index 00000000..abc09ed8 --- /dev/null +++ b/src/Ocelot/Authentication/Handler/Factory/IAuthenticationHandlerFactory.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; +using Ocelot.Responses; + +namespace Ocelot.Authentication.Handler.Factory +{ + using AuthenticationOptions = Configuration.AuthenticationOptions; + + public interface IAuthenticationHandlerFactory + { + Response Get(IApplicationBuilder app, AuthenticationOptions authOptions); + } +} diff --git a/src/Ocelot/Authentication/Handler/Factory/UnableToCreateAuthenticationHandlerError.cs b/src/Ocelot/Authentication/Handler/Factory/UnableToCreateAuthenticationHandlerError.cs new file mode 100644 index 00000000..7e18b203 --- /dev/null +++ b/src/Ocelot/Authentication/Handler/Factory/UnableToCreateAuthenticationHandlerError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Authentication.Handler.Factory +{ + public class UnableToCreateAuthenticationHandlerError : Error + { + public UnableToCreateAuthenticationHandlerError(string message) + : base(message, OcelotErrorCode.UnableToCreateAuthenticationHandlerError) + { + } + } +} diff --git a/src/Ocelot/Authentication/Handler/IHandler.cs b/src/Ocelot/Authentication/Handler/IHandler.cs new file mode 100644 index 00000000..99d240e8 --- /dev/null +++ b/src/Ocelot/Authentication/Handler/IHandler.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Ocelot.Authentication.Handler +{ + public interface IHandler + { + Task Handle(HttpContext context); + } +} \ No newline at end of file diff --git a/src/Ocelot/Authentication/Handler/RequestDelegateHandler.cs b/src/Ocelot/Authentication/Handler/RequestDelegateHandler.cs new file mode 100644 index 00000000..291e8ec3 --- /dev/null +++ b/src/Ocelot/Authentication/Handler/RequestDelegateHandler.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Ocelot.Authentication.Handler +{ + public class RequestDelegateHandler : IHandler + { + private readonly RequestDelegate _requestDelegate; + + public RequestDelegateHandler(RequestDelegate requestDelegate) + { + _requestDelegate = requestDelegate; + } + + public async Task Handle(HttpContext context) + { + await _requestDelegate.Invoke(context); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Authentication/Handler/SupportedAuthenticationProviders.cs b/src/Ocelot/Authentication/Handler/SupportedAuthenticationProviders.cs new file mode 100644 index 00000000..2a815ee0 --- /dev/null +++ b/src/Ocelot/Authentication/Handler/SupportedAuthenticationProviders.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Authentication.Handler +{ + public enum SupportedAuthenticationProviders + { + IdentityServer + } +} diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs new file mode 100644 index 00000000..ad30e166 --- /dev/null +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Authentication.Handler.Factory; +using Ocelot.Configuration; +using Ocelot.Errors; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Authentication.Middleware +{ + public class AuthenticationMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IApplicationBuilder _app; + private readonly IAuthenticationHandlerFactory _authHandlerFactory; + private readonly IOcelotLogger _logger; + + public AuthenticationMiddleware(RequestDelegate next, + IApplicationBuilder app, + IRequestScopedDataRepository requestScopedDataRepository, + IAuthenticationHandlerFactory authHandlerFactory, + IOcelotLoggerFactory loggerFactory) + : base(requestScopedDataRepository) + { + _next = next; + _authHandlerFactory = authHandlerFactory; + _app = app; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started authentication"); + + if (IsAuthenticatedRoute(DownstreamRoute.ReRoute)) + { + var authenticationHandler = _authHandlerFactory.Get(_app, DownstreamRoute.ReRoute.AuthenticationOptions); + + if (!authenticationHandler.IsError) + { + _logger.LogDebug("calling authentication handler for ReRoute"); + + await authenticationHandler.Data.Handler.Handle(context); + } + else + { + _logger.LogDebug("there was an error getting authentication handler for ReRoute"); + + SetPipelineError(authenticationHandler.Errors); + } + + if (context.User.Identity.IsAuthenticated) + { + _logger.LogDebug("the user was authenticated"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + } + else + { + _logger.LogDebug("the user was not authenticated"); + + SetPipelineError(new List { new UnauthenticatedError($"Request for authenticated route {context.Request.Path} by {context.User.Identity.Name} was unauthenticated") }); + } + } + else + { + _logger.LogDebug("calling next middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + } + } + + private static bool IsAuthenticatedRoute(ReRoute reRoute) + { + return reRoute.IsAuthenticated; + } + } +} diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddlewareMiddlewareExtensions.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddlewareMiddlewareExtensions.cs new file mode 100644 index 00000000..8e1a97e8 --- /dev/null +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddlewareMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Authentication.Middleware +{ + public static class AuthenticationMiddlewareMiddlewareExtensions + { + public static IApplicationBuilder UseAuthenticationMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(builder); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Authorisation/ClaimValueNotAuthorisedError.cs b/src/Ocelot/Authorisation/ClaimValueNotAuthorisedError.cs new file mode 100644 index 00000000..166430ed --- /dev/null +++ b/src/Ocelot/Authorisation/ClaimValueNotAuthorisedError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Authorisation +{ + public class ClaimValueNotAuthorisedError : Error + { + public ClaimValueNotAuthorisedError(string message) + : base(message, OcelotErrorCode.ClaimValueNotAuthorisedError) + { + } + } +} diff --git a/src/Ocelot/Authorisation/ClaimsAuthoriser.cs b/src/Ocelot/Authorisation/ClaimsAuthoriser.cs new file mode 100644 index 00000000..cb7849e9 --- /dev/null +++ b/src/Ocelot/Authorisation/ClaimsAuthoriser.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.Authorisation +{ + using Infrastructure.Claims.Parser; + + public class ClaimsAuthoriser : IAuthoriser + { + private readonly IClaimsParser _claimsParser; + + public ClaimsAuthoriser(IClaimsParser claimsParser) + { + _claimsParser = claimsParser; + } + + public Response Authorise(ClaimsPrincipal claimsPrincipal, Dictionary routeClaimsRequirement) + { + foreach (var required in routeClaimsRequirement) + { + var value = _claimsParser.GetValue(claimsPrincipal.Claims, required.Key, string.Empty, 0); + + if (value.IsError) + { + return new ErrorResponse(value.Errors); + } + + if (value.Data != null) + { + var authorised = value.Data == required.Value; + if (!authorised) + { + return new ErrorResponse(new List + { + new ClaimValueNotAuthorisedError( + $"claim value: {value.Data} is not the same as required value: {required.Value} for type: {required.Key}") + }); + } + } + else + { + return new ErrorResponse(new List + { + new UserDoesNotHaveClaimError($"user does not have claim {required.Key}") + }); + } + } + return new OkResponse(true); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Authorisation/IAuthoriser.cs b/src/Ocelot/Authorisation/IAuthoriser.cs new file mode 100644 index 00000000..08a7307f --- /dev/null +++ b/src/Ocelot/Authorisation/IAuthoriser.cs @@ -0,0 +1,13 @@ +using System.Security.Claims; +using Ocelot.Responses; + +namespace Ocelot.Authorisation +{ + using System.Collections.Generic; + + public interface IAuthoriser + { + Response Authorise(ClaimsPrincipal claimsPrincipal, + Dictionary routeClaimsRequirement); + } +} diff --git a/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs b/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs new file mode 100644 index 00000000..547fc7f7 --- /dev/null +++ b/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs @@ -0,0 +1,83 @@ +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Responses; + +namespace Ocelot.Authorisation.Middleware +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Errors; + using Microsoft.AspNetCore.Http; + using Ocelot.Middleware; + + public class AuthorisationMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IAuthoriser _authoriser; + private readonly IOcelotLogger _logger; + + public AuthorisationMiddleware(RequestDelegate next, + IRequestScopedDataRepository requestScopedDataRepository, + IAuthoriser authoriser, + IOcelotLoggerFactory loggerFactory) + : base(requestScopedDataRepository) + { + _next = next; + _authoriser = authoriser; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started authorisation"); + + if (DownstreamRoute.ReRoute.IsAuthorised) + { + _logger.LogDebug("route is authorised"); + + var authorised = _authoriser.Authorise(context.User, DownstreamRoute.ReRoute.RouteClaimsRequirement); + + if (authorised.IsError) + { + _logger.LogDebug("error authorising user"); + + SetPipelineError(authorised.Errors); + return; + } + + if (IsAuthorised(authorised)) + { + _logger.LogDebug("user is authorised calling next middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + } + else + { + _logger.LogDebug("user is not authorised setting pipeline error"); + + SetPipelineError(new List + { + new UnauthorisedError( + $"{context.User.Identity.Name} unable to access {DownstreamRoute.ReRoute.UpstreamTemplate}") + }); + } + } + else + { + _logger.LogDebug("AuthorisationMiddleware.Invoke route is not authorised calling next middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + } + } + + private static bool IsAuthorised(Response authorised) + { + return authorised.Data; + } + } +} diff --git a/src/Ocelot/Authorisation/Middleware/AuthorisationMiddlewareMiddlewareExtensions.cs b/src/Ocelot/Authorisation/Middleware/AuthorisationMiddlewareMiddlewareExtensions.cs new file mode 100644 index 00000000..e2c8af2d --- /dev/null +++ b/src/Ocelot/Authorisation/Middleware/AuthorisationMiddlewareMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Authorisation.Middleware +{ + using Microsoft.AspNetCore.Builder; + + public static class AuthorisationMiddlewareMiddlewareExtensions + { + public static IApplicationBuilder UseAuthorisationMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Authorisation/UnauthorisedError.cs b/src/Ocelot/Authorisation/UnauthorisedError.cs new file mode 100644 index 00000000..766c1a7b --- /dev/null +++ b/src/Ocelot/Authorisation/UnauthorisedError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Authorisation +{ + public class UnauthorisedError : Error + { + public UnauthorisedError(string message) + : base(message, OcelotErrorCode.UnauthorizedError) + { + } + } +} diff --git a/src/Ocelot/Authorisation/UserDoesNotHaveClaimError.cs b/src/Ocelot/Authorisation/UserDoesNotHaveClaimError.cs new file mode 100644 index 00000000..38525f00 --- /dev/null +++ b/src/Ocelot/Authorisation/UserDoesNotHaveClaimError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Authorisation +{ + public class UserDoesNotHaveClaimError : Error + { + public UserDoesNotHaveClaimError(string message) + : base(message, OcelotErrorCode.UserDoesNotHaveClaimError) + { + } + } +} diff --git a/src/Ocelot/Cache/IOcelotCache.cs b/src/Ocelot/Cache/IOcelotCache.cs new file mode 100644 index 00000000..1ceb220c --- /dev/null +++ b/src/Ocelot/Cache/IOcelotCache.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ocelot.Cache +{ + public interface IOcelotCache + { + void Add(string key, T value, TimeSpan ttl); + T Get(string key); + } +} diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs new file mode 100644 index 00000000..77024e70 --- /dev/null +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -0,0 +1,74 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Cache.Middleware +{ + public class OutputCacheMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IOcelotLogger _logger; + private readonly IOcelotCache _outputCache; + + public OutputCacheMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository scopedDataRepository, + IOcelotCache outputCache) + :base(scopedDataRepository) + { + _next = next; + _outputCache = outputCache; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + var downstreamUrlKey = DownstreamUrl; + + if (!DownstreamRoute.ReRoute.IsCached) + { + await _next.Invoke(context); + return; + } + + _logger.LogDebug("started checking cache for {downstreamUrlKey}", downstreamUrlKey); + + var cached = _outputCache.Get(downstreamUrlKey); + + if (cached != null) + { + _logger.LogDebug("cache entry exists for {downstreamUrlKey}", downstreamUrlKey); + + SetHttpResponseMessageThisRequest(cached); + + _logger.LogDebug("finished returned cached response for {downstreamUrlKey}", downstreamUrlKey); + + return; + } + + _logger.LogDebug("no resonse cached for {downstreamUrlKey}", downstreamUrlKey); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + + if (PipelineError) + { + _logger.LogDebug("there was a pipeline error for {downstreamUrlKey}", downstreamUrlKey); + + return; + } + + var response = HttpResponseMessage; + + _outputCache.Add(downstreamUrlKey, response, TimeSpan.FromSeconds(DownstreamRoute.ReRoute.FileCacheOptions.TtlSeconds)); + + _logger.LogDebug("finished response added to cache for {downstreamUrlKey}", downstreamUrlKey); + } + } +} diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddlewareExtensions.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddlewareExtensions.cs new file mode 100644 index 00000000..76e406ee --- /dev/null +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Cache.Middleware +{ + public static class OutputCacheMiddlewareExtensions + { + public static IApplicationBuilder UseOutputCacheMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/src/Ocelot/Cache/OcelotCacheManagerCache.cs b/src/Ocelot/Cache/OcelotCacheManagerCache.cs new file mode 100644 index 00000000..84bdc679 --- /dev/null +++ b/src/Ocelot/Cache/OcelotCacheManagerCache.cs @@ -0,0 +1,25 @@ +using System; +using CacheManager.Core; + +namespace Ocelot.Cache +{ + public class OcelotCacheManagerCache : IOcelotCache + { + private readonly ICacheManager _cacheManager; + + public OcelotCacheManagerCache(ICacheManager cacheManager) + { + _cacheManager = cacheManager; + } + + public void Add(string key, T value, TimeSpan ttl) + { + _cacheManager.Add(new CacheItem(key, value, ExpirationMode.Absolute, ttl)); + } + + public T Get(string key) + { + return _cacheManager.Get(key); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Claims/AddClaimsToRequest.cs b/src/Ocelot/Claims/AddClaimsToRequest.cs new file mode 100644 index 00000000..120b0003 --- /dev/null +++ b/src/Ocelot/Claims/AddClaimsToRequest.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; + +namespace Ocelot.Claims +{ + public class AddClaimsToRequest : IAddClaimsToRequest + { + private readonly IClaimsParser _claimsParser; + + public AddClaimsToRequest(IClaimsParser claimsParser) + { + _claimsParser = claimsParser; + } + + public Response SetClaimsOnContext(List claimsToThings, HttpContext context) + { + foreach (var config in claimsToThings) + { + var value = _claimsParser.GetValue(context.User.Claims, config.NewKey, config.Delimiter, config.Index); + + if (value.IsError) + { + return new ErrorResponse(value.Errors); + } + + var exists = context.User.Claims.FirstOrDefault(x => x.Type == config.ExistingKey); + + var identity = context.User.Identity as ClaimsIdentity; + + if (exists != null) + { + identity?.RemoveClaim(exists); + } + + identity?.AddClaim(new System.Security.Claims.Claim(config.ExistingKey, value.Data)); + } + + return new OkResponse(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Claims/IAddClaimsToRequest.cs b/src/Ocelot/Claims/IAddClaimsToRequest.cs new file mode 100644 index 00000000..02ae85e9 --- /dev/null +++ b/src/Ocelot/Claims/IAddClaimsToRequest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Claims +{ + public interface IAddClaimsToRequest + { + Response SetClaimsOnContext(List claimsToThings, + HttpContext context); + } +} diff --git a/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs b/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs new file mode 100644 index 00000000..1f6486f6 --- /dev/null +++ b/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Claims.Middleware +{ + public class ClaimsBuilderMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IAddClaimsToRequest _addClaimsToRequest; + private readonly IOcelotLogger _logger; + + public ClaimsBuilderMiddleware(RequestDelegate next, + IRequestScopedDataRepository requestScopedDataRepository, + IOcelotLoggerFactory loggerFactory, + IAddClaimsToRequest addClaimsToRequest) + : base(requestScopedDataRepository) + { + _next = next; + _addClaimsToRequest = addClaimsToRequest; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started claims middleware"); + + if (DownstreamRoute.ReRoute.ClaimsToClaims.Any()) + { + _logger.LogDebug("this route has instructions to convert claims to other claims"); + + var result = _addClaimsToRequest.SetClaimsOnContext(DownstreamRoute.ReRoute.ClaimsToClaims, context); + + if (result.IsError) + { + _logger.LogDebug("error converting claims to other claims, setting pipeline error"); + + SetPipelineError(result.Errors); + return; + } + } + + _logger.LogDebug("calling next middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + } + } +} diff --git a/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddlewareExtensions.cs b/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddlewareExtensions.cs new file mode 100644 index 00000000..fd3bef39 --- /dev/null +++ b/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Claims.Middleware +{ + public static class ClaimsBuilderMiddlewareExtensions + { + public static IApplicationBuilder UseClaimsBuilderMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/AuthenticationOptions.cs b/src/Ocelot/Configuration/AuthenticationOptions.cs new file mode 100644 index 00000000..3b36453c --- /dev/null +++ b/src/Ocelot/Configuration/AuthenticationOptions.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration +{ + public class AuthenticationOptions + { + public AuthenticationOptions(string provider, string providerRootUrl, string scopeName, bool requireHttps, List additionalScopes, string scopeSecret) + { + Provider = provider; + ProviderRootUrl = providerRootUrl; + ScopeName = scopeName; + RequireHttps = requireHttps; + AdditionalScopes = additionalScopes; + ScopeSecret = scopeSecret; + } + + public string Provider { get; private set; } + public string ProviderRootUrl { get; private set; } + public string ScopeName { get; private set; } + public string ScopeSecret { get; private set; } + public bool RequireHttps { get; private set; } + public List AdditionalScopes { get; private set; } + + } +} diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs new file mode 100644 index 00000000..1e06a440 --- /dev/null +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using Ocelot.Values; + +namespace Ocelot.Configuration.Builder +{ + public class ReRouteBuilder + { + private string _downstreamPathTemplate; + private string _upstreamTemplate; + private string _upstreamTemplatePattern; + private string _upstreamHttpMethod; + private bool _isAuthenticated; + private string _authenticationProvider; + private string _authenticationProviderUrl; + private string _scopeName; + private List _additionalScopes; + private bool _requireHttps; + private string _scopeSecret; + private List _configHeaderExtractorProperties; + private List _claimToClaims; + private Dictionary _routeClaimRequirement; + private bool _isAuthorised; + private List _claimToQueries; + private string _requestIdHeaderKey; + private bool _isCached; + private CacheOptions _fileCacheOptions; + private bool _useServiceDiscovery; + private string _serviceName; + private string _serviceDiscoveryProvider; + private string _serviceDiscoveryAddress; + private string _downstreamScheme; + private string _downstreamHost; + private int _dsPort; + + public ReRouteBuilder() + { + _additionalScopes = new List(); + } + + public ReRouteBuilder WithDownstreamScheme(string downstreamScheme) + { + _downstreamScheme = downstreamScheme; + return this; + } + + public ReRouteBuilder WithDownstreamHost(string downstreamHost) + { + _downstreamHost = downstreamHost; + return this; + } + + public ReRouteBuilder WithServiceDiscoveryAddress(string serviceDiscoveryAddress) + { + _serviceDiscoveryAddress = serviceDiscoveryAddress; + return this; + } + + public ReRouteBuilder WithServiceDiscoveryProvider(string serviceDiscoveryProvider) + { + _serviceDiscoveryProvider = serviceDiscoveryProvider; + return this; + } + + public ReRouteBuilder WithServiceName(string serviceName) + { + _serviceName = serviceName; + return this; + } + + public ReRouteBuilder WithUseServiceDiscovery(bool useServiceDiscovery) + { + _useServiceDiscovery = useServiceDiscovery; + return this; + } + + public ReRouteBuilder WithDownstreamPathTemplate(string input) + { + _downstreamPathTemplate = input; + return this; + } + + public ReRouteBuilder WithUpstreamTemplate(string input) + { + _upstreamTemplate = input; + return this; + } + + public ReRouteBuilder WithUpstreamTemplatePattern(string input) + { + _upstreamTemplatePattern = input; + return this; + } + public ReRouteBuilder WithUpstreamHttpMethod(string input) + { + _upstreamHttpMethod = input; + return this; + } + public ReRouteBuilder WithIsAuthenticated(bool input) + { + _isAuthenticated = input; + return this; + } + + public ReRouteBuilder WithIsAuthorised(bool input) + { + _isAuthorised = input; + return this; + } + + public ReRouteBuilder WithAuthenticationProvider(string input) + { + _authenticationProvider = input; + return this; + } + + public ReRouteBuilder WithAuthenticationProviderUrl(string input) + { + _authenticationProviderUrl = input; + return this; + } + + public ReRouteBuilder WithAuthenticationProviderScopeName(string input) + { + _scopeName = input; + return this; + } + + public ReRouteBuilder WithAuthenticationProviderAdditionalScopes(List input) + { + _additionalScopes = input; + return this; + } + + public ReRouteBuilder WithRequireHttps(bool input) + { + _requireHttps = input; + return this; + } + + public ReRouteBuilder WithScopeSecret(string input) + { + _scopeSecret = input; + return this; + } + + public ReRouteBuilder WithRequestIdKey(string input) + { + _requestIdHeaderKey = input; + return this; + } + + public ReRouteBuilder WithClaimsToHeaders(List input) + { + _configHeaderExtractorProperties = input; + return this; + } + + public ReRouteBuilder WithClaimsToClaims(List input) + { + _claimToClaims = input; + return this; + } + + public ReRouteBuilder WithRouteClaimsRequirement(Dictionary input) + { + _routeClaimRequirement = input; + return this; + } + + public ReRouteBuilder WithClaimsToQueries(List input) + { + _claimToQueries = input; + return this; + } + + public ReRouteBuilder WithIsCached(bool input) + { + _isCached = input; + return this; + } + + public ReRouteBuilder WithCacheOptions(CacheOptions input) + { + _fileCacheOptions = input; + return this; + } + + public ReRouteBuilder WithDownstreamPort(int port) + { + _dsPort = port; + return this; + } + + public ReRoute Build() + { + Func downstreamHostFunc = () => new HostAndPort(_downstreamHost, _dsPort); + + return new ReRoute(new DownstreamPathTemplate(_downstreamPathTemplate), _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, + _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, + _requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement, + _isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions, _serviceName, + _useServiceDiscovery, _serviceDiscoveryAddress, _serviceDiscoveryProvider, downstreamHostFunc, _downstreamScheme); + } + } +} diff --git a/src/Ocelot/Configuration/CacheOptions.cs b/src/Ocelot/Configuration/CacheOptions.cs new file mode 100644 index 00000000..2fdaf2bb --- /dev/null +++ b/src/Ocelot/Configuration/CacheOptions.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Configuration +{ + public class CacheOptions + { + public CacheOptions(int ttlSeconds) + { + TtlSeconds = ttlSeconds; + } + + public int TtlSeconds { get; private set; } + } +} diff --git a/src/Ocelot/Configuration/ClaimToThing.cs b/src/Ocelot/Configuration/ClaimToThing.cs new file mode 100644 index 00000000..5a677667 --- /dev/null +++ b/src/Ocelot/Configuration/ClaimToThing.cs @@ -0,0 +1,18 @@ +namespace Ocelot.Configuration +{ + public class ClaimToThing + { + public ClaimToThing(string existingKey, string newKey, string delimiter, int index) + { + NewKey = newKey; + Delimiter = delimiter; + Index = index; + ExistingKey = existingKey; + } + + public string ExistingKey { get; private set; } + public string NewKey { get; private set; } + public string Delimiter { get; private set; } + public int Index { get; private set; } + } +} diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs new file mode 100644 index 00000000..8884f0d9 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Parser; +using Ocelot.Configuration.Validator; +using Ocelot.Responses; +using Ocelot.Utilities; +using Ocelot.Values; + +namespace Ocelot.Configuration.Creator +{ + /// + /// Register as singleton + /// + public class FileOcelotConfigurationCreator : IOcelotConfigurationCreator + { + private readonly IOptions _options; + private readonly IConfigurationValidator _configurationValidator; + private const string RegExMatchEverything = ".*"; + private const string RegExMatchEndString = "$"; + private const string RegExIgnoreCase = "(?i)"; + + private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; + private readonly ILogger _logger; + + public FileOcelotConfigurationCreator( + IOptions options, + IConfigurationValidator configurationValidator, + IClaimToThingConfigurationParser claimToThingConfigurationParser, + ILogger logger) + { + _options = options; + _configurationValidator = configurationValidator; + _claimToThingConfigurationParser = claimToThingConfigurationParser; + _logger = logger; + } + + public Response Create() + { + var config = SetUpConfiguration(); + + return new OkResponse(config); + } + + /// + /// This method is meant to be tempoary to convert a config to an ocelot config...probably wont keep this but we will see + /// will need a refactor at some point as its crap + /// + private IOcelotConfiguration SetUpConfiguration() + { + var response = _configurationValidator.IsValid(_options.Value); + + if (response.Data.IsError) + { + var errorBuilder = new StringBuilder(); + + foreach (var error in response.Errors) + { + errorBuilder.AppendLine(error.Message); + } + + throw new Exception($"Unable to start Ocelot..configuration, errors were {errorBuilder}"); + } + + var reRoutes = new List(); + + foreach (var reRoute in _options.Value.ReRoutes) + { + var ocelotReRoute = SetUpReRoute(reRoute, _options.Value.GlobalConfiguration); + reRoutes.Add(ocelotReRoute); + } + + return new OcelotConfiguration(reRoutes); + } + + private ReRoute SetUpReRoute(FileReRoute reRoute, FileGlobalConfiguration globalConfiguration) + { + var globalRequestIdConfiguration = !string.IsNullOrEmpty(globalConfiguration?.RequestIdKey); + + var upstreamTemplate = BuildUpstreamTemplate(reRoute); + + var isAuthenticated = !string.IsNullOrEmpty(reRoute.AuthenticationOptions?.Provider); + + var isAuthorised = reRoute.RouteClaimsRequirement?.Count > 0; + + var isCached = reRoute.FileCacheOptions.TtlSeconds > 0; + + var requestIdKey = globalRequestIdConfiguration + ? globalConfiguration.RequestIdKey + : reRoute.RequestIdKey; + + var useServiceDiscovery = !string.IsNullOrEmpty(reRoute.ServiceName) + && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Address) + && !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Provider); + + + Func downstreamHostAndPortFunc = () => new HostAndPort(reRoute.DownstreamHost.Trim('/'), reRoute.DownstreamPort); + + if (isAuthenticated) + { + var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider, + reRoute.AuthenticationOptions.ProviderRootUrl, reRoute.AuthenticationOptions.ScopeName, + reRoute.AuthenticationOptions.RequireHttps, reRoute.AuthenticationOptions.AdditionalScopes, + reRoute.AuthenticationOptions.ScopeSecret); + + var claimsToHeaders = GetAddThingsToRequest(reRoute.AddHeadersToRequest); + var claimsToClaims = GetAddThingsToRequest(reRoute.AddClaimsToRequest); + var claimsToQueries = GetAddThingsToRequest(reRoute.AddQueriesToRequest); + + return new ReRoute(new DownstreamPathTemplate(reRoute.DownstreamPathTemplate), reRoute.UpstreamTemplate, + reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, + authOptionsForRoute, claimsToHeaders, claimsToClaims, + reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries, + requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds), + reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider, + globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostAndPortFunc, reRoute.DownstreamScheme); + } + + return new ReRoute(new DownstreamPathTemplate(reRoute.DownstreamPathTemplate), reRoute.UpstreamTemplate, + reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated, + null, new List(), new List(), + reRoute.RouteClaimsRequirement, isAuthorised, new List(), + requestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds), + reRoute.ServiceName, useServiceDiscovery, globalConfiguration?.ServiceDiscoveryProvider?.Provider, + globalConfiguration?.ServiceDiscoveryProvider?.Address, downstreamHostAndPortFunc, reRoute.DownstreamScheme); + } + + private string BuildUpstreamTemplate(FileReRoute reRoute) + { + var upstreamTemplate = reRoute.UpstreamTemplate; + + upstreamTemplate = upstreamTemplate.SetLastCharacterAs('/'); + + var placeholders = new List(); + + for (var i = 0; i < upstreamTemplate.Length; i++) + { + if (IsPlaceHolder(upstreamTemplate, i)) + { + var postitionOfPlaceHolderClosingBracket = upstreamTemplate.IndexOf('}', i); + var difference = postitionOfPlaceHolderClosingBracket - i + 1; + var variableName = upstreamTemplate.Substring(i, difference); + placeholders.Add(variableName); + } + } + + foreach (var placeholder in placeholders) + { + upstreamTemplate = upstreamTemplate.Replace(placeholder, RegExMatchEverything); + } + + var route = reRoute.ReRouteIsCaseSensitive + ? $"{upstreamTemplate}{RegExMatchEndString}" + : $"{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; + + return route; + } + + private List GetAddThingsToRequest(Dictionary thingBeingAdded) + { + var claimsToTHings = new List(); + + foreach (var add in thingBeingAdded) + { + var claimToHeader = _claimToThingConfigurationParser.Extract(add.Key, add.Value); + + if (claimToHeader.IsError) + { + _logger.LogCritical(new EventId(1, "Application Failed to start"), + $"Unable to extract configuration for key: {add.Key} and value: {add.Value} your configuration file is incorrect"); + + throw new Exception(claimToHeader.Errors[0].Message); + } + claimsToTHings.Add(claimToHeader.Data); + } + + return claimsToTHings; + } + + private bool IsPlaceHolder(string upstreamTemplate, int i) + { + return upstreamTemplate[i] == '{'; + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs new file mode 100644 index 00000000..6cc7c2e8 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs @@ -0,0 +1,9 @@ +using Ocelot.Responses; + +namespace Ocelot.Configuration.Creator +{ + public interface IOcelotConfigurationCreator + { + Response Create(); + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs new file mode 100644 index 00000000..0904d87e --- /dev/null +++ b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration.File +{ + public class FileAuthenticationOptions + { + public FileAuthenticationOptions() + { + AdditionalScopes = new List(); + } + + public string Provider { get; set; } + public string ProviderRootUrl { get; set; } + public string ScopeName { get; set; } + public bool RequireHttps { get; set; } + public List AdditionalScopes { get; set; } + public string ScopeSecret { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileCacheOptions.cs b/src/Ocelot/Configuration/File/FileCacheOptions.cs new file mode 100644 index 00000000..3f86006b --- /dev/null +++ b/src/Ocelot/Configuration/File/FileCacheOptions.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Configuration.File +{ + public class FileCacheOptions + { + public int TtlSeconds { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileConfiguration.cs b/src/Ocelot/Configuration/File/FileConfiguration.cs new file mode 100644 index 00000000..18938a0e --- /dev/null +++ b/src/Ocelot/Configuration/File/FileConfiguration.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration.File +{ + public class FileConfiguration + { + public FileConfiguration() + { + ReRoutes = new List(); + GlobalConfiguration = new FileGlobalConfiguration(); + } + + public List ReRoutes { get; set; } + public FileGlobalConfiguration GlobalConfiguration { get; set; } + } +} diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs new file mode 100644 index 00000000..f414bc83 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Configuration.File +{ + public class FileGlobalConfiguration + { + public FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider(); + } + public string RequestIdKey { get; set; } + public FileServiceDiscoveryProvider ServiceDiscoveryProvider {get;set;} + } +} diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs new file mode 100644 index 00000000..a653224a --- /dev/null +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration.File +{ + public class FileReRoute + { + public FileReRoute() + { + AddHeadersToRequest = new Dictionary(); + AddClaimsToRequest = new Dictionary(); + RouteClaimsRequirement = new Dictionary(); + AddQueriesToRequest = new Dictionary(); + AuthenticationOptions = new FileAuthenticationOptions(); + FileCacheOptions = new FileCacheOptions(); + } + + public string DownstreamPathTemplate { get; set; } + public string UpstreamTemplate { get; set; } + public string UpstreamHttpMethod { get; set; } + public FileAuthenticationOptions AuthenticationOptions { get; set; } + public Dictionary AddHeadersToRequest { get; set; } + public Dictionary AddClaimsToRequest { get; set; } + public Dictionary RouteClaimsRequirement { get; set; } + public Dictionary AddQueriesToRequest { get; set; } + public string RequestIdKey { get; set; } + public FileCacheOptions FileCacheOptions { get; set; } + public bool ReRouteIsCaseSensitive { get; set; } + public string ServiceName { get; set; } + public string DownstreamScheme {get;set;} + public string DownstreamHost {get;set;} + public int DownstreamPort { get; set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs new file mode 100644 index 00000000..47efc6df --- /dev/null +++ b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs @@ -0,0 +1,8 @@ +namespace Ocelot.Configuration.File +{ + public class FileServiceDiscoveryProvider + { + public string Provider {get;set;} + public string Address {get;set;} + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/IOcelotConfiguration.cs b/src/Ocelot/Configuration/IOcelotConfiguration.cs new file mode 100644 index 00000000..8359a2e1 --- /dev/null +++ b/src/Ocelot/Configuration/IOcelotConfiguration.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration +{ + public interface IOcelotConfiguration + { + List ReRoutes { get; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/OcelotConfiguration.cs b/src/Ocelot/Configuration/OcelotConfiguration.cs new file mode 100644 index 00000000..3b0858eb --- /dev/null +++ b/src/Ocelot/Configuration/OcelotConfiguration.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Ocelot.Configuration +{ + public class OcelotConfiguration : IOcelotConfiguration + { + public OcelotConfiguration(List reRoutes) + { + ReRoutes = reRoutes; + } + + public List ReRoutes { get; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs b/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs new file mode 100644 index 00000000..45dfe62d --- /dev/null +++ b/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Parser +{ + public class ClaimToThingConfigurationParser : IClaimToThingConfigurationParser + { + private readonly Regex _claimRegex = new Regex("Claims\\[.*\\]"); + private readonly Regex _indexRegex = new Regex("value\\[.*\\]"); + private const string SplitToken = ">"; + + public Response Extract(string existingKey, string value) + { + try + { + var instructions = value.Split(SplitToken.ToCharArray()); + + if (instructions.Length <= 1) + { + return new ErrorResponse( + new List + { + new NoInstructionsError(SplitToken) + }); + } + + var claimMatch = _claimRegex.IsMatch(instructions[0]); + + if (!claimMatch) + { + return new ErrorResponse( + new List + { + new InstructionNotForClaimsError() + }); + } + + var newKey = GetIndexValue(instructions[0]); + var index = 0; + var delimiter = string.Empty; + + if (instructions.Length > 2 && _indexRegex.IsMatch(instructions[1])) + { + index = int.Parse(GetIndexValue(instructions[1])); + delimiter = instructions[2].Trim(); + } + + return new OkResponse( + new ClaimToThing(existingKey, newKey, delimiter, index)); + } + catch (Exception exception) + { + return new ErrorResponse( + new List + { + new ParsingConfigurationHeaderError(exception) + }); + } + } + + private string GetIndexValue(string instruction) + { + var firstIndexer = instruction.IndexOf("[", StringComparison.Ordinal); + var lastIndexer = instruction.IndexOf("]", StringComparison.Ordinal); + var length = lastIndexer - firstIndexer; + var claimKey = instruction.Substring(firstIndexer + 1, length - 1); + return claimKey; + } + } +} diff --git a/src/Ocelot/Configuration/Parser/IClaimToThingConfigurationParser.cs b/src/Ocelot/Configuration/Parser/IClaimToThingConfigurationParser.cs new file mode 100644 index 00000000..a2712994 --- /dev/null +++ b/src/Ocelot/Configuration/Parser/IClaimToThingConfigurationParser.cs @@ -0,0 +1,9 @@ +using Ocelot.Responses; + +namespace Ocelot.Configuration.Parser +{ + public interface IClaimToThingConfigurationParser + { + Response Extract(string existingKey, string value); + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Parser/InstructionNotForClaimsError.cs b/src/Ocelot/Configuration/Parser/InstructionNotForClaimsError.cs new file mode 100644 index 00000000..62cf8741 --- /dev/null +++ b/src/Ocelot/Configuration/Parser/InstructionNotForClaimsError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Configuration.Parser +{ + public class InstructionNotForClaimsError : Error + { + public InstructionNotForClaimsError() + : base("instructions did not contain claims, at the moment we only support claims extraction", OcelotErrorCode.InstructionNotForClaimsError) + { + } + } +} diff --git a/src/Ocelot/Configuration/Parser/NoInstructionsError.cs b/src/Ocelot/Configuration/Parser/NoInstructionsError.cs new file mode 100644 index 00000000..09aa0ccb --- /dev/null +++ b/src/Ocelot/Configuration/Parser/NoInstructionsError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Configuration.Parser +{ + public class NoInstructionsError : Error + { + public NoInstructionsError(string splitToken) + : base($"There we no instructions splitting on {splitToken}", OcelotErrorCode.NoInstructionsError) + { + } + } +} diff --git a/src/Ocelot/Configuration/Parser/ParsingConfigurationHeaderError.cs b/src/Ocelot/Configuration/Parser/ParsingConfigurationHeaderError.cs new file mode 100644 index 00000000..9b03c95d --- /dev/null +++ b/src/Ocelot/Configuration/Parser/ParsingConfigurationHeaderError.cs @@ -0,0 +1,13 @@ +using System; +using Ocelot.Errors; + +namespace Ocelot.Configuration.Parser +{ + public class ParsingConfigurationHeaderError : Error + { + public ParsingConfigurationHeaderError(Exception exception) + : base($"error parsing configuration eception is {exception.Message}", OcelotErrorCode.ParsingConfigurationHeaderError) + { + } + } +} diff --git a/src/Ocelot/Configuration/Provider/IOcelotConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/IOcelotConfigurationProvider.cs new file mode 100644 index 00000000..30ded2e9 --- /dev/null +++ b/src/Ocelot/Configuration/Provider/IOcelotConfigurationProvider.cs @@ -0,0 +1,9 @@ +using Ocelot.Responses; + +namespace Ocelot.Configuration.Provider +{ + public interface IOcelotConfigurationProvider + { + Response Get(); + } +} diff --git a/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs new file mode 100644 index 00000000..4b6c5fd2 --- /dev/null +++ b/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs @@ -0,0 +1,48 @@ +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.Repository; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Provider +{ + /// + /// Register as singleton + /// + public class OcelotConfigurationProvider : IOcelotConfigurationProvider + { + private readonly IOcelotConfigurationRepository _repo; + private readonly IOcelotConfigurationCreator _creator; + + public OcelotConfigurationProvider(IOcelotConfigurationRepository repo, + IOcelotConfigurationCreator creator) + { + _repo = repo; + _creator = creator; + } + + public Response Get() + { + var repoConfig = _repo.Get(); + + if (repoConfig.IsError) + { + return new ErrorResponse(repoConfig.Errors); + } + + if (repoConfig.Data == null) + { + var creatorConfig = _creator.Create(); + + if (creatorConfig.IsError) + { + return new ErrorResponse(creatorConfig.Errors); + } + + _repo.AddOrReplace(creatorConfig.Data); + + return new OkResponse(creatorConfig.Data); + } + + return new OkResponse(repoConfig.Data); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs new file mode 100644 index 00000000..960374cc --- /dev/null +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Ocelot.Values; + +namespace Ocelot.Configuration +{ + public class ReRoute + { + public ReRoute(DownstreamPathTemplate downstreamPathTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, + bool isAuthenticated, AuthenticationOptions authenticationOptions, List configurationHeaderExtractorProperties, + List claimsToClaims, Dictionary routeClaimsRequirement, bool isAuthorised, List claimsToQueries, + string requestIdKey, bool isCached, CacheOptions fileCacheOptions, string serviceName, bool useServiceDiscovery, + string serviceDiscoveryProvider, string serviceDiscoveryAddress, Func downstreamHostAndPort, string downstreamScheme) + { + DownstreamPathTemplate = downstreamPathTemplate; + UpstreamTemplate = upstreamTemplate; + UpstreamHttpMethod = upstreamHttpMethod; + UpstreamTemplatePattern = upstreamTemplatePattern; + IsAuthenticated = isAuthenticated; + AuthenticationOptions = authenticationOptions; + RouteClaimsRequirement = routeClaimsRequirement; + IsAuthorised = isAuthorised; + RequestIdKey = requestIdKey; + IsCached = isCached; + FileCacheOptions = fileCacheOptions; + ClaimsToQueries = claimsToQueries + ?? new List(); + ClaimsToClaims = claimsToClaims + ?? new List(); + ClaimsToHeaders = configurationHeaderExtractorProperties + ?? new List(); + ServiceName = serviceName; + UseServiceDiscovery = useServiceDiscovery; + ServiceDiscoveryProvider = serviceDiscoveryProvider; + ServiceDiscoveryAddress = serviceDiscoveryAddress; + DownstreamHostAndPort = downstreamHostAndPort; + DownstreamScheme = downstreamScheme; + } + + public DownstreamPathTemplate DownstreamPathTemplate { get; private set; } + public string UpstreamTemplate { get; private set; } + public string UpstreamTemplatePattern { get; private set; } + public string UpstreamHttpMethod { get; private set; } + public bool IsAuthenticated { get; private set; } + public bool IsAuthorised { get; private set; } + public AuthenticationOptions AuthenticationOptions { get; private set; } + public List ClaimsToQueries { get; private set; } + public List ClaimsToHeaders { get; private set; } + public List ClaimsToClaims { get; private set; } + public Dictionary RouteClaimsRequirement { get; private set; } + public string RequestIdKey { get; private set; } + public bool IsCached { get; private set; } + public CacheOptions FileCacheOptions { get; private set; } + public string ServiceName { get; private set;} + public bool UseServiceDiscovery { get; private set;} + public string ServiceDiscoveryProvider { get; private set;} + public string ServiceDiscoveryAddress { get; private set;} + public Func DownstreamHostAndPort {get;private set;} + public string DownstreamScheme {get;private set;} + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Repository/IOcelotConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/IOcelotConfigurationRepository.cs new file mode 100644 index 00000000..312b5553 --- /dev/null +++ b/src/Ocelot/Configuration/Repository/IOcelotConfigurationRepository.cs @@ -0,0 +1,10 @@ +using Ocelot.Responses; + +namespace Ocelot.Configuration.Repository +{ + public interface IOcelotConfigurationRepository + { + Response Get(); + Response AddOrReplace(IOcelotConfiguration ocelotConfiguration); + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Repository/InMemoryOcelotConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/InMemoryOcelotConfigurationRepository.cs new file mode 100644 index 00000000..04f34f8d --- /dev/null +++ b/src/Ocelot/Configuration/Repository/InMemoryOcelotConfigurationRepository.cs @@ -0,0 +1,29 @@ +using Ocelot.Responses; + +namespace Ocelot.Configuration.Repository +{ + /// + /// Register as singleton + /// + public class InMemoryOcelotConfigurationRepository : IOcelotConfigurationRepository + { + private static readonly object LockObject = new object(); + + private IOcelotConfiguration _ocelotConfiguration; + + public Response Get() + { + return new OkResponse(_ocelotConfiguration); + } + + public Response AddOrReplace(IOcelotConfiguration ocelotConfiguration) + { + lock (LockObject) + { + _ocelotConfiguration = ocelotConfiguration; + } + + return new OkResponse(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs b/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs new file mode 100644 index 00000000..32dec98b --- /dev/null +++ b/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class ConfigurationValidationResult + { + public ConfigurationValidationResult(bool isError) + { + IsError = isError; + Errors = new List(); + } + + public ConfigurationValidationResult(bool isError, List errors) + { + IsError = isError; + Errors = errors; + } + + public bool IsError { get; private set; } + + public List Errors { get; private set; } + } +} diff --git a/src/Ocelot/Configuration/Validator/DownstreamPathTemplateAlreadyUsedError.cs b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateAlreadyUsedError.cs new file mode 100644 index 00000000..e350753c --- /dev/null +++ b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateAlreadyUsedError.cs @@ -0,0 +1,11 @@ +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class DownstreamPathTemplateAlreadyUsedError : Error + { + public DownstreamPathTemplateAlreadyUsedError(string message) : base(message, OcelotErrorCode.DownstreampathTemplateAlreadyUsedError) + { + } + } +} diff --git a/src/Ocelot/Configuration/Validator/DownstreamPathTemplateContainsSchemeError.cs b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateContainsSchemeError.cs new file mode 100644 index 00000000..a3dfa309 --- /dev/null +++ b/src/Ocelot/Configuration/Validator/DownstreamPathTemplateContainsSchemeError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class DownstreamPathTemplateContainsSchemeError : Error + { + public DownstreamPathTemplateContainsSchemeError(string message) + : base(message, OcelotErrorCode.DownstreamPathTemplateContainsSchemeError) + { + } + } +} diff --git a/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs b/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs new file mode 100644 index 00000000..412613eb --- /dev/null +++ b/src/Ocelot/Configuration/Validator/FileConfigurationValidator.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Ocelot.Authentication.Handler; +using Ocelot.Configuration.File; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Validator +{ + public class FileConfigurationValidator : IConfigurationValidator + { + public Response IsValid(FileConfiguration configuration) + { + var result = CheckForDupliateReRoutes(configuration); + + if (result.IsError) + { + return new OkResponse(result); + } + + result = CheckForUnsupportedAuthenticationProviders(configuration); + + if (result.IsError) + { + return new OkResponse(result); + } + + result = CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(configuration); + + if (result.IsError) + { + return new OkResponse(result); + } + + return new OkResponse(result); + } + + private ConfigurationValidationResult CheckForUnsupportedAuthenticationProviders(FileConfiguration configuration) + { + var errors = new List(); + + foreach (var reRoute in configuration.ReRoutes) + { + var isAuthenticated = !string.IsNullOrEmpty(reRoute.AuthenticationOptions?.Provider); + + if (!isAuthenticated) + { + continue; + } + + if (IsSupportedAuthenticationProvider(reRoute.AuthenticationOptions?.Provider)) + { + continue; + } + + var error = new UnsupportedAuthenticationProviderError($"{reRoute.AuthenticationOptions?.Provider} is unsupported authentication provider, upstream template is {reRoute.UpstreamTemplate}, upstream method is {reRoute.UpstreamHttpMethod}"); + errors.Add(error); + } + + return errors.Count > 0 + ? new ConfigurationValidationResult(true, errors) + : new ConfigurationValidationResult(false); + } + + private bool IsSupportedAuthenticationProvider(string provider) + { + SupportedAuthenticationProviders supportedProvider; + + return Enum.TryParse(provider, true, out supportedProvider); + } + + private ConfigurationValidationResult CheckForReRoutesContainingDownstreamSchemeInDownstreamPathTemplate(FileConfiguration configuration) + { + var errors = new List(); + + foreach(var reRoute in configuration.ReRoutes) + { + if(reRoute.DownstreamPathTemplate.Contains("https://") + || reRoute.DownstreamPathTemplate.Contains("http://")) + { + errors.Add(new DownstreamPathTemplateContainsSchemeError($"{reRoute.DownstreamPathTemplate} contains scheme")); + } + } + + if(errors.Any()) + { + return new ConfigurationValidationResult(true, errors); + } + + return new ConfigurationValidationResult(false, errors); + } + + private ConfigurationValidationResult CheckForDupliateReRoutes(FileConfiguration configuration) + { + var hasDupes = configuration.ReRoutes + .GroupBy(x => new { x.UpstreamTemplate, x.UpstreamHttpMethod }).Any(x => x.Skip(1).Any()); + + if (!hasDupes) + { + return new ConfigurationValidationResult(false); + } + + var dupes = configuration.ReRoutes.GroupBy(x => new { x.UpstreamTemplate, x.UpstreamHttpMethod }) + .Where(x => x.Skip(1).Any()); + + var errors = dupes + .Select(d => new DownstreamPathTemplateAlreadyUsedError(string.Format("Duplicate DownstreamPath: {0}", d.Key.UpstreamTemplate))) + .Cast() + .ToList(); + + return new ConfigurationValidationResult(true, errors); + } + } +} diff --git a/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs b/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs new file mode 100644 index 00000000..09bf7dae --- /dev/null +++ b/src/Ocelot/Configuration/Validator/IConfigurationValidator.cs @@ -0,0 +1,10 @@ +using Ocelot.Configuration.File; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Validator +{ + public interface IConfigurationValidator + { + Response IsValid(FileConfiguration configuration); + } +} diff --git a/src/Ocelot/Configuration/Validator/UnsupportedAuthenticationProviderError.cs b/src/Ocelot/Configuration/Validator/UnsupportedAuthenticationProviderError.cs new file mode 100644 index 00000000..e4f441bf --- /dev/null +++ b/src/Ocelot/Configuration/Validator/UnsupportedAuthenticationProviderError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Configuration.Validator +{ + public class UnsupportedAuthenticationProviderError : Error + { + public UnsupportedAuthenticationProviderError(string message) + : base(message, OcelotErrorCode.UnsupportedAuthenticationProviderError) + { + } + } +} diff --git a/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..9f40b009 --- /dev/null +++ b/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Net.Http; +using CacheManager.Core; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Ocelot.Authentication.Handler.Creator; +using Ocelot.Authentication.Handler.Factory; +using Ocelot.Authorisation; +using Ocelot.Cache; +using Ocelot.Claims; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Parser; +using Ocelot.Configuration.Provider; +using Ocelot.Configuration.Repository; +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.Logging; +using Ocelot.QueryStrings; +using Ocelot.Request.Builder; +using Ocelot.Requester; +using Ocelot.Responder; + +namespace Ocelot.DependencyInjection +{ + public static class ServiceCollectionExtensions + { + + public static IServiceCollection AddOcelotOutputCaching(this IServiceCollection services, Action settings) + { + var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", settings); + var ocelotCacheManager = new OcelotCacheManagerCache(cacheManagerOutputCache); + + services.AddSingleton>(cacheManagerOutputCache); + services.AddSingleton>(ocelotCacheManager); + + return services; + } + public static IServiceCollection AddOcelotFileConfiguration(this IServiceCollection services, IConfigurationRoot configurationRoot) + { + services.Configure(configurationRoot); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } + + public static IServiceCollection AddOcelot(this IServiceCollection services) + { + services.AddMvcCore().AddJsonFormatters(); + services.AddLogging(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc + // could maybe use a scoped data repository + services.AddSingleton(); + services.AddScoped(); + + return services; + } + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs b/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs new file mode 100644 index 00000000..d4a117c0 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/DownstreamRoute.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Ocelot.Configuration; +using Ocelot.DownstreamRouteFinder.UrlMatcher; + +namespace Ocelot.DownstreamRouteFinder +{ + public class DownstreamRoute + { + public DownstreamRoute(List templatePlaceholderNameAndValues, ReRoute reRoute) + { + TemplatePlaceholderNameAndValues = templatePlaceholderNameAndValues; + ReRoute = reRoute; + } + public List TemplatePlaceholderNameAndValues { get; private set; } + public ReRoute ReRoute { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs new file mode 100644 index 00000000..752da281 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Ocelot.Configuration.Provider; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.DownstreamRouteFinder.Finder +{ + public class DownstreamRouteFinder : IDownstreamRouteFinder + { + private readonly IOcelotConfigurationProvider _configProvider; + private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; + private readonly IUrlPathPlaceholderNameAndValueFinder _urlPathPlaceholderNameAndValueFinder; + + public DownstreamRouteFinder(IOcelotConfigurationProvider configProvider, IUrlPathToUrlTemplateMatcher urlMatcher, IUrlPathPlaceholderNameAndValueFinder urlPathPlaceholderNameAndValueFinder) + { + _configProvider = configProvider; + _urlMatcher = urlMatcher; + _urlPathPlaceholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; + } + + public Response FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod) + { + var configuration = _configProvider.Get(); + + var applicableReRoutes = configuration.Data.ReRoutes.Where(r => string.Equals(r.UpstreamHttpMethod, upstreamHttpMethod, StringComparison.CurrentCultureIgnoreCase)); + + foreach (var reRoute in applicableReRoutes) + { + var urlMatch = _urlMatcher.Match(upstreamUrlPath, reRoute.UpstreamTemplatePattern); + + if (urlMatch.Data.Match) + { + var templateVariableNameAndValues = _urlPathPlaceholderNameAndValueFinder.Find(upstreamUrlPath, reRoute.UpstreamTemplate); + + return new OkResponse(new DownstreamRoute(templateVariableNameAndValues.Data, reRoute)); + } + } + + return new ErrorResponse(new List + { + new UnableToFindDownstreamRouteError() + }); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs new file mode 100644 index 00000000..e351ab2f --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs @@ -0,0 +1,9 @@ +using Ocelot.Responses; + +namespace Ocelot.DownstreamRouteFinder.Finder +{ + public interface IDownstreamRouteFinder + { + Response FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod); + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs b/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs new file mode 100644 index 00000000..d73587b1 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs @@ -0,0 +1,11 @@ +using Ocelot.Errors; + +namespace Ocelot.DownstreamRouteFinder.Finder +{ + public class UnableToFindDownstreamRouteError : Error + { + public UnableToFindDownstreamRouteError() : base("UnableToFindDownstreamRouteError", OcelotErrorCode.UnableToFindDownstreamRouteError) + { + } + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs new file mode 100644 index 00000000..f445b46b --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -0,0 +1,59 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Utilities; + +namespace Ocelot.DownstreamRouteFinder.Middleware +{ + public class DownstreamRouteFinderMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IDownstreamRouteFinder _downstreamRouteFinder; + private readonly IOcelotLogger _logger; + + public DownstreamRouteFinderMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IDownstreamRouteFinder downstreamRouteFinder, + IRequestScopedDataRepository requestScopedDataRepository) + :base(requestScopedDataRepository) + { + _next = next; + _downstreamRouteFinder = downstreamRouteFinder; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started calling downstream route finder middleware"); + + var upstreamUrlPath = context.Request.Path.ToString().SetLastCharacterAs('/'); + + _logger.LogDebug("upstream url path is {upstreamUrlPath}", upstreamUrlPath); + + var downstreamRoute = _downstreamRouteFinder.FindDownstreamRoute(upstreamUrlPath, context.Request.Method); + + if (downstreamRoute.IsError) + { + _logger.LogDebug("IDownstreamRouteFinder returned an error, setting pipeline error"); + + SetPipelineError(downstreamRoute.Errors); + return; + } + + _logger.LogDebug("downstream template is {downstreamRoute.Data.ReRoute.DownstreamPath}", downstreamRoute.Data.ReRoute.DownstreamPathTemplate); + + SetDownstreamRouteForThisRequest(downstreamRoute.Data); + + _logger.LogDebug("calling next middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddlewareExtensions.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddlewareExtensions.cs new file mode 100644 index 00000000..0cd27758 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.DownstreamRouteFinder.Middleware +{ + public static class DownstreamRouteFinderMiddlewareExtensions + { + public static IApplicationBuilder UseDownstreamRouteFinderMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathPlaceholderNameAndValueFinder.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathPlaceholderNameAndValueFinder.cs new file mode 100644 index 00000000..788299cb --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathPlaceholderNameAndValueFinder.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Ocelot.Responses; + +namespace Ocelot.DownstreamRouteFinder.UrlMatcher +{ + public interface IUrlPathPlaceholderNameAndValueFinder + { + Response> Find(string upstreamUrlPath, string upstreamUrlPathTemplate); + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathToUrlTemplateMatcher.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathToUrlTemplateMatcher.cs new file mode 100644 index 00000000..6c8956a2 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathToUrlTemplateMatcher.cs @@ -0,0 +1,9 @@ +using Ocelot.Responses; + +namespace Ocelot.DownstreamRouteFinder.UrlMatcher +{ + public interface IUrlPathToUrlTemplateMatcher + { + Response Match(string upstreamUrlPath, string upstreamUrlPathTemplate); + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcher.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcher.cs new file mode 100644 index 00000000..415ee556 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcher.cs @@ -0,0 +1,17 @@ +using System.Text.RegularExpressions; +using Ocelot.Responses; + +namespace Ocelot.DownstreamRouteFinder.UrlMatcher +{ + public class RegExUrlMatcher : IUrlPathToUrlTemplateMatcher + { + public Response Match(string upstreamUrlPath, string upstreamUrlPathTemplate) + { + var regex = new Regex(upstreamUrlPathTemplate); + + return regex.IsMatch(upstreamUrlPath) + ? new OkResponse(new UrlMatch(true)) + : new OkResponse(new UrlMatch(false)); + } + } +} diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlMatch.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlMatch.cs new file mode 100644 index 00000000..aa2a6f06 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlMatch.cs @@ -0,0 +1,11 @@ +namespace Ocelot.DownstreamRouteFinder.UrlMatcher +{ + public class UrlMatch + { + public UrlMatch(bool match) + { + Match = match; + } + public bool Match {get;private set;} + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValue.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValue.cs new file mode 100644 index 00000000..cb690666 --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValue.cs @@ -0,0 +1,13 @@ +namespace Ocelot.DownstreamRouteFinder.UrlMatcher +{ + public class UrlPathPlaceholderNameAndValue + { + public UrlPathPlaceholderNameAndValue(string templateVariableName, string templateVariableValue) + { + TemplateVariableName = templateVariableName; + TemplateVariableValue = templateVariableValue; + } + public string TemplateVariableName {get;private set;} + public string TemplateVariableValue {get;private set;} + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs new file mode 100644 index 00000000..249b153e --- /dev/null +++ b/src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using Ocelot.Responses; + +namespace Ocelot.DownstreamRouteFinder.UrlMatcher +{ + public class UrlPathPlaceholderNameAndValueFinder : IUrlPathPlaceholderNameAndValueFinder + { + public Response> Find(string upstreamUrlPath, string upstreamUrlPathTemplate) + { + var templateKeysAndValues = new List(); + + int counterForUrl = 0; + + for (int counterForTemplate = 0; counterForTemplate < upstreamUrlPathTemplate.Length; counterForTemplate++) + { + if (CharactersDontMatch(upstreamUrlPathTemplate[counterForTemplate], upstreamUrlPath[counterForUrl]) && ContinueScanningUrl(counterForUrl,upstreamUrlPath.Length)) + { + if (IsPlaceholder(upstreamUrlPathTemplate[counterForTemplate])) + { + var variableName = GetPlaceholderVariableName(upstreamUrlPathTemplate, counterForTemplate); + + var variableValue = GetPlaceholderVariableValue(upstreamUrlPath, counterForUrl); + + var templateVariableNameAndValue = new UrlPathPlaceholderNameAndValue(variableName, variableValue); + + templateKeysAndValues.Add(templateVariableNameAndValue); + + counterForTemplate = GetNextCounterPosition(upstreamUrlPathTemplate, counterForTemplate, '}'); + + counterForUrl = GetNextCounterPosition(upstreamUrlPath, counterForUrl, '/'); + + continue; + } + + return new OkResponse>(templateKeysAndValues); + } + counterForUrl++; + } + + return new OkResponse>(templateKeysAndValues); + } + + private string GetPlaceholderVariableValue(string urlPath, int counterForUrl) + { + var positionOfNextSlash = urlPath.IndexOf('/', counterForUrl); + + if(positionOfNextSlash == -1) + { + positionOfNextSlash = urlPath.Length; + } + + var variableValue = urlPath.Substring(counterForUrl, positionOfNextSlash - counterForUrl); + + return variableValue; + } + + private string GetPlaceholderVariableName(string urlPathTemplate, int counterForTemplate) + { + var postitionOfPlaceHolderClosingBracket = urlPathTemplate.IndexOf('}', counterForTemplate) + 1; + + var variableName = urlPathTemplate.Substring(counterForTemplate, postitionOfPlaceHolderClosingBracket - counterForTemplate); + + return variableName; + } + private int GetNextCounterPosition(string urlTemplate, int counterForTemplate, char delimiter) + { + var closingPlaceHolderPositionOnTemplate = urlTemplate.IndexOf(delimiter, counterForTemplate); + return closingPlaceHolderPositionOnTemplate + 1; + } + + private bool CharactersDontMatch(char characterOne, char characterTwo) + { + return characterOne != characterTwo; + } + + private bool ContinueScanningUrl(int counterForUrl, int urlLength) + { + return counterForUrl < urlLength; + } + + private bool IsPlaceholder(char character) + { + return character == '{'; + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/DownstreamHostNullOrEmptyError.cs b/src/Ocelot/DownstreamUrlCreator/DownstreamHostNullOrEmptyError.cs new file mode 100644 index 00000000..8978f665 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/DownstreamHostNullOrEmptyError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.DownstreamUrlCreator +{ + public class DownstreamHostNullOrEmptyError : Error + { + public DownstreamHostNullOrEmptyError() + : base("downstream host was null or empty", OcelotErrorCode.DownstreamHostNullOrEmptyError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/DownstreamPathNullOrEmptyError.cs b/src/Ocelot/DownstreamUrlCreator/DownstreamPathNullOrEmptyError.cs new file mode 100644 index 00000000..fbc1a5f5 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/DownstreamPathNullOrEmptyError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.DownstreamUrlCreator +{ + public class DownstreamPathNullOrEmptyError : Error + { + public DownstreamPathNullOrEmptyError() + : base("downstream path was null or empty", OcelotErrorCode.DownstreamPathNullOrEmptyError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/DownstreamSchemeNullOrEmptyError.cs b/src/Ocelot/DownstreamUrlCreator/DownstreamSchemeNullOrEmptyError.cs new file mode 100644 index 00000000..e52d3488 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/DownstreamSchemeNullOrEmptyError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.DownstreamUrlCreator +{ + public class DownstreamSchemeNullOrEmptyError : Error + { + public DownstreamSchemeNullOrEmptyError() + : base("downstream scheme was null or empty", OcelotErrorCode.DownstreamSchemeNullOrEmptyError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/IUrlBuilder.cs b/src/Ocelot/DownstreamUrlCreator/IUrlBuilder.cs new file mode 100644 index 00000000..18683e62 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/IUrlBuilder.cs @@ -0,0 +1,12 @@ +using Ocelot.Configuration; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.DownstreamUrlCreator +{ + public interface IUrlBuilder + { + Response Build(string downstreamPath, string downstreamScheme, HostAndPort downstreamHostAndPort); + } +} diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs new file mode 100644 index 00000000..8144b42b --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -0,0 +1,72 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Configuration; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.DownstreamUrlCreator.Middleware +{ + public class DownstreamUrlCreatorMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IDownstreamPathPlaceholderReplacer _replacer; + private readonly IOcelotLogger _logger; + private readonly IUrlBuilder _urlBuilder; + + public DownstreamUrlCreatorMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IDownstreamPathPlaceholderReplacer replacer, + IRequestScopedDataRepository requestScopedDataRepository, + IUrlBuilder urlBuilder) + :base(requestScopedDataRepository) + { + _next = next; + _replacer = replacer; + _urlBuilder = urlBuilder; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started calling downstream url creator middleware"); + + var dsPath = _replacer + .Replace(DownstreamRoute.ReRoute.DownstreamPathTemplate, DownstreamRoute.TemplatePlaceholderNameAndValues); + + if (dsPath.IsError) + { + _logger.LogDebug("IDownstreamPathPlaceholderReplacer returned an error, setting pipeline error"); + + SetPipelineError(dsPath.Errors); + return; + } + + var dsScheme = DownstreamRoute.ReRoute.DownstreamScheme; + + var dsHostAndPort = DownstreamRoute.ReRoute.DownstreamHostAndPort(); + + 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"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs new file mode 100644 index 00000000..34bfaa54 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.DownstreamUrlCreator.Middleware +{ + public static class DownstreamUrlCreatorMiddlewareExtensions + { + public static IApplicationBuilder UseDownstreamUrlCreatorMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/UrlBuilder.cs b/src/Ocelot/DownstreamUrlCreator/UrlBuilder.cs new file mode 100644 index 00000000..2124ce3b --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/UrlBuilder.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Ocelot.Configuration; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Errors; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.DownstreamUrlCreator +{ + public class UrlBuilder : IUrlBuilder + { + public Response Build(string downstreamPath, string downstreamScheme, HostAndPort downstreamHostAndPort) + { + if (string.IsNullOrEmpty(downstreamPath)) + { + return new ErrorResponse(new List {new DownstreamPathNullOrEmptyError()}); + } + + if (string.IsNullOrEmpty(downstreamScheme)) + { + return new ErrorResponse(new List { new DownstreamSchemeNullOrEmptyError() }); + } + + if (string.IsNullOrEmpty(downstreamHostAndPort.DownstreamHost)) + { + return new ErrorResponse(new List { new DownstreamHostNullOrEmptyError() }); + } + + var builder = new UriBuilder + { + Host = downstreamHostAndPort.DownstreamHost, + Path = downstreamPath, + Scheme = downstreamScheme + }; + + if (downstreamHostAndPort.DownstreamPort > 0) + { + builder.Port = downstreamHostAndPort.DownstreamPort; + } + + var url = builder.Uri.ToString(); + + return new OkResponse(new DownstreamUrl(url)); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs new file mode 100644 index 00000000..9e925631 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamUrlTemplateVariableReplacer.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Text; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer +{ + public class DownstreamTemplatePathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer + { + public Response Replace(DownstreamPathTemplate downstreamPathTemplate, List urlPathPlaceholderNameAndValues) + { + var downstreamPath = new StringBuilder(); + + downstreamPath.Append(downstreamPathTemplate.Value); + + foreach (var placeholderVariableAndValue in urlPathPlaceholderNameAndValues) + { + downstreamPath.Replace(placeholderVariableAndValue.TemplateVariableName, placeholderVariableAndValue.TemplateVariableValue); + } + + return new OkResponse(new DownstreamPath(downstreamPath.ToString())); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs new file mode 100644 index 00000000..72d5d4b6 --- /dev/null +++ b/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamUrlPathTemplateVariableReplacer.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Responses; +using Ocelot.Values; + +namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer +{ + public interface IDownstreamPathPlaceholderReplacer + { + Response Replace(DownstreamPathTemplate downstreamPathTemplate, List urlPathPlaceholderNameAndValues); + } +} \ No newline at end of file diff --git a/src/Ocelot/Errors/Error.cs b/src/Ocelot/Errors/Error.cs new file mode 100644 index 00000000..25a9f5d4 --- /dev/null +++ b/src/Ocelot/Errors/Error.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Errors +{ + public abstract class Error + { + protected Error(string message, OcelotErrorCode code) + { + Message = message; + Code = code; + } + + public string Message { get; private set; } + public OcelotErrorCode Code { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs new file mode 100644 index 00000000..fc3ab200 --- /dev/null +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -0,0 +1,72 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; + +namespace Ocelot.Errors.Middleware +{ + public class ExceptionHandlerMiddleware + { + private readonly RequestDelegate _next; + private readonly IOcelotLogger _logger; + private readonly IRequestScopedDataRepository _requestScopedDataRepository; + + public ExceptionHandlerMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository) + { + _next = next; + _requestScopedDataRepository = requestScopedDataRepository; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + try + { + _logger.LogDebug("ocelot pipeline started"); + + _logger.LogDebug("calling next middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called middleware"); + } + catch (Exception e) + { + _logger.LogDebug("error calling middleware"); + + var message = CreateMessage(context, e); + _logger.LogError(message, e); + await SetInternalServerErrorOnResponse(context); + } + + _logger.LogDebug("ocelot pipeline finished"); + } + + private async Task SetInternalServerErrorOnResponse(HttpContext context) + { + context.Response.OnStarting(x => + { + context.Response.StatusCode = 500; + return Task.CompletedTask; + }, context); + } + + private string CreateMessage(HttpContext context, Exception e) + { + var message = + $"Exception caught in global error handler, exception message: {e.Message}, exception stack: {e.StackTrace}"; + + if (e.InnerException != null) + { + message = + $"{message}, inner exception message {e.InnerException.Message}, inner exception stack {e.InnerException.StackTrace}"; + } + return $"{message} RequestId: {context.TraceIdentifier}"; + } + } +} diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddlewareExtensions.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddlewareExtensions.cs new file mode 100644 index 00000000..e355c7f8 --- /dev/null +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Errors.Middleware +{ + public static class ExceptionHandlerMiddlewareExtensions + { + public static IApplicationBuilder UseExceptionHandlerMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs new file mode 100644 index 00000000..5de770cd --- /dev/null +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -0,0 +1,26 @@ +namespace Ocelot.Errors +{ + public enum OcelotErrorCode + { + UnauthenticatedError, + UnknownError, + DownstreampathTemplateAlreadyUsedError, + UnableToFindDownstreamRouteError, + CannotAddDataError, + CannotFindDataError, + UnableToCompleteRequestError, + UnableToCreateAuthenticationHandlerError, + UnsupportedAuthenticationProviderError, + CannotFindClaimError, + ParsingConfigurationHeaderError, + NoInstructionsError, + InstructionNotForClaimsError, + UnauthorizedError, + ClaimValueNotAuthorisedError, + UserDoesNotHaveClaimError, + DownstreamPathTemplateContainsSchemeError, + DownstreamPathNullOrEmptyError, + DownstreamSchemeNullOrEmptyError, + DownstreamHostNullOrEmptyError + } +} diff --git a/src/Ocelot/Headers/AddHeadersToRequest.cs b/src/Ocelot/Headers/AddHeadersToRequest.cs new file mode 100644 index 00000000..97cd3e69 --- /dev/null +++ b/src/Ocelot/Headers/AddHeadersToRequest.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Ocelot.Configuration; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public class AddHeadersToRequest : IAddHeadersToRequest + { + private readonly IClaimsParser _claimsParser; + + public AddHeadersToRequest(IClaimsParser claimsParser) + { + _claimsParser = claimsParser; + } + + public Response SetHeadersOnContext(List claimsToThings, HttpContext context) + { + foreach (var config in claimsToThings) + { + var value = _claimsParser.GetValue(context.User.Claims, config.NewKey, config.Delimiter, config.Index); + + if (value.IsError) + { + return new ErrorResponse(value.Errors); + } + + var exists = context.Request.Headers.FirstOrDefault(x => x.Key == config.ExistingKey); + + if (!string.IsNullOrEmpty(exists.Key)) + { + context.Request.Headers.Remove(exists); + } + + context.Request.Headers.Add(config.ExistingKey, new StringValues(value.Data)); + } + + return new OkResponse(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/IAddHeadersToRequest.cs b/src/Ocelot/Headers/IAddHeadersToRequest.cs new file mode 100644 index 00000000..3bf786a4 --- /dev/null +++ b/src/Ocelot/Headers/IAddHeadersToRequest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public interface IAddHeadersToRequest + { + Response SetHeadersOnContext(List claimsToThings, + HttpContext context); + } +} diff --git a/src/Ocelot/Headers/IRemoveOutputHeaders.cs b/src/Ocelot/Headers/IRemoveOutputHeaders.cs new file mode 100644 index 00000000..909d78f9 --- /dev/null +++ b/src/Ocelot/Headers/IRemoveOutputHeaders.cs @@ -0,0 +1,10 @@ +using System.Net.Http.Headers; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + public interface IRemoveOutputHeaders + { + Response Remove(HttpResponseHeaders headers); + } +} diff --git a/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs new file mode 100644 index 00000000..cc262048 --- /dev/null +++ b/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs @@ -0,0 +1,56 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Headers.Middleware +{ + public class HttpRequestHeadersBuilderMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IAddHeadersToRequest _addHeadersToRequest; + private readonly IOcelotLogger _logger; + + public HttpRequestHeadersBuilderMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository, + IAddHeadersToRequest addHeadersToRequest) + : base(requestScopedDataRepository) + { + _next = next; + _addHeadersToRequest = addHeadersToRequest; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started calling headers builder middleware"); + + if (DownstreamRoute.ReRoute.ClaimsToHeaders.Any()) + { + _logger.LogDebug("this route has instructions to convert claims to headers"); + + var response = _addHeadersToRequest.SetHeadersOnContext(DownstreamRoute.ReRoute.ClaimsToHeaders, context); + + if (response.IsError) + { + _logger.LogDebug("there was an error setting headers on context, setting pipeline error"); + + SetPipelineError(response.Errors); + return; + } + + _logger.LogDebug("headers have been set on context"); + } + + _logger.LogDebug("calling next middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + } + } +} diff --git a/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddlewareExtensions.cs b/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddlewareExtensions.cs new file mode 100644 index 00000000..ce010b14 --- /dev/null +++ b/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Headers.Middleware +{ + public static class HttpRequestHeadersBuilderMiddlewareExtensions + { + public static IApplicationBuilder UseHttpRequestHeadersBuilderMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Headers/RemoveOutputHeaders.cs b/src/Ocelot/Headers/RemoveOutputHeaders.cs new file mode 100644 index 00000000..7a40534b --- /dev/null +++ b/src/Ocelot/Headers/RemoveOutputHeaders.cs @@ -0,0 +1,29 @@ +using System.Net.Http.Headers; +using Ocelot.Responses; + +namespace Ocelot.Headers +{ + using System; + + public class RemoveOutputHeaders : IRemoveOutputHeaders + { + /// + /// Some webservers return headers that cannot be forwarded to the client + /// in a given context such as transfer encoding chunked when ASP.NET is not + /// returning the response in this manner + /// + private readonly string[] _unsupportedRequestHeaders = + { + "Transfer-Encoding" + }; + public Response Remove(HttpResponseHeaders headers) + { + foreach (var unsupported in _unsupportedRequestHeaders) + { + headers.Remove(unsupported); + } + + return new OkResponse(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Infrastructure/Claims/Parser/CannotFindClaimError.cs b/src/Ocelot/Infrastructure/Claims/Parser/CannotFindClaimError.cs new file mode 100644 index 00000000..64a91cb8 --- /dev/null +++ b/src/Ocelot/Infrastructure/Claims/Parser/CannotFindClaimError.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Infrastructure.Claims.Parser +{ + using Errors; + + public class CannotFindClaimError : Error + { + public CannotFindClaimError(string message) + : base(message, OcelotErrorCode.CannotFindClaimError) + { + } + } +} diff --git a/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs b/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs new file mode 100644 index 00000000..e37f48fa --- /dev/null +++ b/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs @@ -0,0 +1,55 @@ +namespace Ocelot.Infrastructure.Claims.Parser +{ + using System.Collections.Generic; + using System.Linq; + using System.Security.Claims; + using Errors; + using Responses; + + public class ClaimsParser : IClaimsParser + { + public Response GetValue(IEnumerable claims, string key, string delimiter, int index) + { + var claimResponse = GetValue(claims, key); + + if (claimResponse.IsError) + { + return claimResponse; + } + + if (string.IsNullOrEmpty(delimiter)) + { + return claimResponse; + } + + var splits = claimResponse.Data.Split(delimiter.ToCharArray()); + + if (splits.Length < index || index < 0) + { + return new ErrorResponse(new List + { + new CannotFindClaimError($"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}") + }); + } + + var value = splits[index]; + + return new OkResponse(value); + } + + private Response GetValue(IEnumerable claims, string key) + { + var claim = claims.FirstOrDefault(c => c.Type == key); + + if (claim != null) + { + return new OkResponse(claim.Value); + } + + return new ErrorResponse(new List + { + new CannotFindClaimError($"Cannot find claim for key: {key}") + }); + } + } +} diff --git a/src/Ocelot/Infrastructure/Claims/Parser/IClaimsParser.cs b/src/Ocelot/Infrastructure/Claims/Parser/IClaimsParser.cs new file mode 100644 index 00000000..fa94cd22 --- /dev/null +++ b/src/Ocelot/Infrastructure/Claims/Parser/IClaimsParser.cs @@ -0,0 +1,11 @@ +namespace Ocelot.Infrastructure.Claims.Parser +{ + using System.Collections.Generic; + using System.Security.Claims; + using Responses; + + public interface IClaimsParser + { + Response GetValue(IEnumerable claims, string key, string delimiter, int index); + } +} \ No newline at end of file diff --git a/src/Ocelot/Infrastructure/RequestData/CannotAddDataError.cs b/src/Ocelot/Infrastructure/RequestData/CannotAddDataError.cs new file mode 100644 index 00000000..806a47e7 --- /dev/null +++ b/src/Ocelot/Infrastructure/RequestData/CannotAddDataError.cs @@ -0,0 +1,11 @@ +using Ocelot.Errors; + +namespace Ocelot.Infrastructure.RequestData +{ + public class CannotAddDataError : Error + { + public CannotAddDataError(string message) : base(message, OcelotErrorCode.CannotAddDataError) + { + } + } +} diff --git a/src/Ocelot/Infrastructure/RequestData/CannotFindDataError.cs b/src/Ocelot/Infrastructure/RequestData/CannotFindDataError.cs new file mode 100644 index 00000000..75a85129 --- /dev/null +++ b/src/Ocelot/Infrastructure/RequestData/CannotFindDataError.cs @@ -0,0 +1,11 @@ +using Ocelot.Errors; + +namespace Ocelot.Infrastructure.RequestData +{ + public class CannotFindDataError : Error + { + public CannotFindDataError(string message) : base(message, OcelotErrorCode.CannotFindDataError) + { + } + } +} diff --git a/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs b/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs new file mode 100644 index 00000000..0f15ca7e --- /dev/null +++ b/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.Infrastructure.RequestData +{ + public class HttpDataRepository : IRequestScopedDataRepository + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public HttpDataRepository(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public Response Add(string key, T value) + { + try + { + _httpContextAccessor.HttpContext.Items.Add(key, value); + return new OkResponse(); + } + catch (Exception exception) + { + return new ErrorResponse(new List + { + new CannotAddDataError(string.Format($"Unable to add data for key: {key}, exception: {exception.Message}")) + }); + } + } + + public Response Get(string key) + { + object obj; + + if(_httpContextAccessor.HttpContext.Items.TryGetValue(key, out obj)) + { + var data = (T) obj; + return new OkResponse(data); + } + + return new ErrorResponse(new List + { + new CannotFindDataError($"Unable to find data for key: {key}") + }); + } + } +} diff --git a/src/Ocelot/Infrastructure/RequestData/IRequestScopedDataRepository.cs b/src/Ocelot/Infrastructure/RequestData/IRequestScopedDataRepository.cs new file mode 100644 index 00000000..f707178c --- /dev/null +++ b/src/Ocelot/Infrastructure/RequestData/IRequestScopedDataRepository.cs @@ -0,0 +1,10 @@ +using Ocelot.Responses; + +namespace Ocelot.Infrastructure.RequestData +{ + public interface IRequestScopedDataRepository + { + Response Add(string key, T value); + Response Get(string key); + } +} \ No newline at end of file diff --git a/src/Ocelot/Logging/IOcelotLoggerFactory.cs b/src/Ocelot/Logging/IOcelotLoggerFactory.cs new file mode 100644 index 00000000..5305088f --- /dev/null +++ b/src/Ocelot/Logging/IOcelotLoggerFactory.cs @@ -0,0 +1,69 @@ +using System; +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; + +namespace Ocelot.Logging +{ + public interface IOcelotLoggerFactory + { + IOcelotLogger CreateLogger(); + } + + public class AspDotNetLoggerFactory : IOcelotLoggerFactory + { + private readonly ILoggerFactory _loggerFactory; + private readonly IRequestScopedDataRepository _scopedDataRepository; + + public AspDotNetLoggerFactory(ILoggerFactory loggerFactory, IRequestScopedDataRepository scopedDataRepository) + { + _loggerFactory = loggerFactory; + _scopedDataRepository = scopedDataRepository; + } + + public IOcelotLogger CreateLogger() + { + var logger = _loggerFactory.CreateLogger(); + return new AspDotNetLogger(logger, _scopedDataRepository); + } + } + + public interface IOcelotLogger + { + void LogDebug(string message, params object[] args); + void LogError(string message, Exception exception); + } + + public class AspDotNetLogger : IOcelotLogger + { + private readonly ILogger _logger; + private readonly IRequestScopedDataRepository _scopedDataRepository; + + public AspDotNetLogger(ILogger logger, IRequestScopedDataRepository scopedDataRepository) + { + _logger = logger; + _scopedDataRepository = scopedDataRepository; + } + + public void LogDebug(string message, params object[] args) + { + _logger.LogDebug(GetMessageWithOcelotRequestId(message), args); + } + + public void LogError(string message, Exception exception) + { + _logger.LogError(GetMessageWithOcelotRequestId(message), exception); + } + + private string GetMessageWithOcelotRequestId(string message) + { + var requestId = _scopedDataRepository.Get("RequestId"); + + if (requestId != null && !requestId.IsError) + { + return $"{message} : OcelotRequestId - {requestId.Data}"; + + } + return $"{message} : OcelotRequestId - not set"; + } + } +} diff --git a/src/Ocelot/Middleware/OcelotMiddleware.cs b/src/Ocelot/Middleware/OcelotMiddleware.cs new file mode 100644 index 00000000..0bb51040 --- /dev/null +++ b/src/Ocelot/Middleware/OcelotMiddleware.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Net.Http; +using Ocelot.DownstreamRouteFinder; +using Ocelot.Errors; +using Ocelot.Infrastructure.RequestData; + +namespace Ocelot.Middleware +{ + public abstract class OcelotMiddleware + { + private readonly IRequestScopedDataRepository _requestScopedDataRepository; + + protected OcelotMiddleware(IRequestScopedDataRepository requestScopedDataRepository) + { + _requestScopedDataRepository = requestScopedDataRepository; + } + + public bool PipelineError + { + get + { + var response = _requestScopedDataRepository.Get("OcelotMiddlewareError"); + return response.Data; + } + } + + public List PipelineErrors + { + get + { + var response = _requestScopedDataRepository.Get>("OcelotMiddlewareErrors"); + return response.Data; + } + } + + public DownstreamRoute DownstreamRoute + { + get + { + var downstreamRoute = _requestScopedDataRepository.Get("DownstreamRoute"); + return downstreamRoute.Data; + } + } + + public string DownstreamUrl + { + get + { + var downstreamUrl = _requestScopedDataRepository.Get("DownstreamUrl"); + return downstreamUrl.Data; + } + } + + public Request.Request Request + { + get + { + var request = _requestScopedDataRepository.Get("Request"); + return request.Data; + } + } + + public HttpResponseMessage HttpResponseMessage + { + get + { + var request = _requestScopedDataRepository.Get("HttpResponseMessage"); + return request.Data; + } + } + + public void SetDownstreamRouteForThisRequest(DownstreamRoute downstreamRoute) + { + _requestScopedDataRepository.Add("DownstreamRoute", downstreamRoute); + } + + public void SetDownstreamUrlForThisRequest(string downstreamUrl) + { + _requestScopedDataRepository.Add("DownstreamUrl", downstreamUrl); + } + + public void SetUpstreamRequestForThisRequest(Request.Request request) + { + _requestScopedDataRepository.Add("Request", request); + } + + public void SetHttpResponseMessageThisRequest(HttpResponseMessage responseMessage) + { + _requestScopedDataRepository.Add("HttpResponseMessage", responseMessage); + } + + public void SetPipelineError(List errors) + { + _requestScopedDataRepository.Add("OcelotMiddlewareError", true); + _requestScopedDataRepository.Add("OcelotMiddlewareErrors", errors); + } + } +} diff --git a/src/Ocelot/Middleware/OcelotMiddlewareConfiguration.cs b/src/Ocelot/Middleware/OcelotMiddlewareConfiguration.cs new file mode 100644 index 00000000..555f8df9 --- /dev/null +++ b/src/Ocelot/Middleware/OcelotMiddlewareConfiguration.cs @@ -0,0 +1,44 @@ +namespace Ocelot.Middleware +{ + using System; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + + public class OcelotMiddlewareConfiguration + { + /// + /// This is called after the global error handling middleware so any code before calling next.invoke + /// is the next thing called in the Ocelot pipeline. Anything after next.invoke is the last thing called + /// in the Ocelot pipeline before we go to the global error handler. + /// + public Func, Task> PreErrorResponderMiddleware { get; set; } + + /// + /// This is to allow the user to run any extra authentication before the Ocelot authentication + /// kicks in + /// + public Func, Task> PreAuthenticationMiddleware { get; set; } + + /// + /// This allows the user to completely override the ocelot authentication middleware + /// + public Func, Task> AuthenticationMiddleware { get; set; } + + /// + /// This is to allow the user to run any extra authorisation before the Ocelot authentication + /// kicks in + /// + public Func, Task> PreAuthorisationMiddleware { get; set; } + + /// + /// This allows the user to completely override the ocelot authorisation middleware + /// + public Func, Task> AuthorisationMiddleware { get; set; } + + /// + /// This allows the user to implement there own query string manipulation logic + /// + public Func, Task> PreQueryStringBuilderMiddleware { get; set; } + + } +} \ No newline at end of file diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs new file mode 100644 index 00000000..dfa3b3f4 --- /dev/null +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.Builder; +using Ocelot.Authentication.Middleware; +using Ocelot.Cache.Middleware; +using Ocelot.Claims.Middleware; +using Ocelot.DownstreamRouteFinder.Middleware; +using Ocelot.DownstreamUrlCreator.Middleware; +using Ocelot.Errors.Middleware; +using Ocelot.Headers.Middleware; +using Ocelot.QueryStrings.Middleware; +using Ocelot.Request.Middleware; +using Ocelot.Requester.Middleware; +using Ocelot.RequestId.Middleware; +using Ocelot.Responder.Middleware; + +namespace Ocelot.Middleware +{ + using System; + using System.Threading.Tasks; + using Authorisation.Middleware; + using Microsoft.AspNetCore.Http; + + public static class OcelotMiddlewareExtensions + { + /// + /// Registers the Ocelot default middlewares + /// + /// + /// + public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder) + { + builder.UseOcelot(new OcelotMiddlewareConfiguration()); + return builder; + } + + /// + /// Registers Ocelot with a combination of default middlewares and optional middlewares in the configuration + /// + /// + /// + /// + public static IApplicationBuilder UseOcelot(this IApplicationBuilder builder, OcelotMiddlewareConfiguration middlewareConfiguration) + { + // This is registered to catch any global exceptions that are not handled + builder.UseExceptionHandlerMiddleware(); + + // Allow the user to respond with absolutely anything they want. + builder.UseIfNotNull(middlewareConfiguration.PreErrorResponderMiddleware); + + // This is registered first so it can catch any errors and issue an appropriate response + builder.UseResponderMiddleware(); + + // Then we get the downstream route information + builder.UseDownstreamRouteFinderMiddleware(); + + // Now we can look for the requestId + builder.UseRequestIdMiddleware(); + + // Allow pre authentication logic. The idea being people might want to run something custom before what is built in. + builder.UseIfNotNull(middlewareConfiguration.PreAuthenticationMiddleware); + + // Now we know where the client is going to go we can authenticate them. + // We allow the ocelot middleware to be overriden by whatever the + // user wants + if (middlewareConfiguration.AuthenticationMiddleware == null) + { + builder.UseAuthenticationMiddleware(); + } + else + { + builder.Use(middlewareConfiguration.AuthenticationMiddleware); + } + + // The next thing we do is look at any claims transforms in case this is important for authorisation + builder.UseClaimsBuilderMiddleware(); + + // Allow pre authorisation logic. The idea being people might want to run something custom before what is built in. + builder.UseIfNotNull(middlewareConfiguration.PreAuthorisationMiddleware); + + // Now we have authenticated and done any claims transformation we + // can authorise the request + // We allow the ocelot middleware to be overriden by whatever the + // user wants + if (middlewareConfiguration.AuthorisationMiddleware == null) + { + builder.UseAuthorisationMiddleware(); + } + else + { + builder.Use(middlewareConfiguration.AuthorisationMiddleware); + } + + // Now we can run any header transformation logic + builder.UseHttpRequestHeadersBuilderMiddleware(); + + // Allow the user to implement their own query string manipulation logic + builder.UseIfNotNull(middlewareConfiguration.PreQueryStringBuilderMiddleware); + + // Now we can run any query string transformation logic + builder.UseQueryStringBuilderMiddleware(); + + // This takes the downstream route we retrieved earlier and replaces any placeholders with the variables that should be used + builder.UseDownstreamUrlCreatorMiddleware(); + + // Not sure if this is the best place for this but we use the downstream url + // as the basis for our cache key. + builder.UseOutputCacheMiddleware(); + + // Everything should now be ready to build or HttpRequest + builder.UseHttpRequestBuilderMiddleware(); + + //We fire off the request and set the response on the scoped data repo + builder.UseHttpRequesterMiddleware(); + + return builder; + } + + private static void UseIfNotNull(this IApplicationBuilder builder, Func, Task> middleware) + { + if (middleware != null) + { + builder.Use(middleware); + } + } + } +} diff --git a/src/Ocelot/Middleware/UnauthenticatedError.cs b/src/Ocelot/Middleware/UnauthenticatedError.cs new file mode 100644 index 00000000..c8b04039 --- /dev/null +++ b/src/Ocelot/Middleware/UnauthenticatedError.cs @@ -0,0 +1,11 @@ +using Ocelot.Errors; + +namespace Ocelot.Middleware +{ + public class UnauthenticatedError : Error + { + public UnauthenticatedError(string message) : base(message, OcelotErrorCode.UnauthenticatedError) + { + } + } +} diff --git a/src/Ocelot/Ocelot.xproj b/src/Ocelot/Ocelot.xproj new file mode 100644 index 00000000..5f2a7cc5 --- /dev/null +++ b/src/Ocelot/Ocelot.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + d6df4206-0dba-41d8-884d-c3e08290fdbb + Ocelot + .\obj + .\bin\ + v4.5 + + + 2.0 + + + \ No newline at end of file diff --git a/src/Ocelot/Properties/AssemblyInfo.cs b/src/Ocelot/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..ad12027e --- /dev/null +++ b/src/Ocelot/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Ocelot")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("d6df4206-0dba-41d8-884d-c3e08290fdbb")] diff --git a/src/Ocelot/QueryStrings/AddQueriesToRequest.cs b/src/Ocelot/QueryStrings/AddQueriesToRequest.cs new file mode 100644 index 00000000..02fcb63d --- /dev/null +++ b/src/Ocelot/QueryStrings/AddQueriesToRequest.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; + +namespace Ocelot.QueryStrings +{ + public class AddQueriesToRequest : IAddQueriesToRequest + { + private readonly IClaimsParser _claimsParser; + + public AddQueriesToRequest(IClaimsParser claimsParser) + { + _claimsParser = claimsParser; + } + + public Response SetQueriesOnContext(List claimsToThings, HttpContext context) + { + var queryDictionary = ConvertQueryStringToDictionary(context); + + foreach (var config in claimsToThings) + { + var value = _claimsParser.GetValue(context.User.Claims, config.NewKey, config.Delimiter, config.Index); + + if (value.IsError) + { + return new ErrorResponse(value.Errors); + } + + var exists = queryDictionary.FirstOrDefault(x => x.Key == config.ExistingKey); + + if (!string.IsNullOrEmpty(exists.Key)) + { + queryDictionary[exists.Key] = value.Data; + } + else + { + queryDictionary.Add(config.ExistingKey, value.Data); + } + } + + context.Request.QueryString = ConvertDictionaryToQueryString(queryDictionary); + + return new OkResponse(); + } + + private Dictionary ConvertQueryStringToDictionary(HttpContext context) + { + return Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(context.Request.QueryString.Value) + .ToDictionary(q => q.Key, q => q.Value.FirstOrDefault() ?? string.Empty); + } + + private Microsoft.AspNetCore.Http.QueryString ConvertDictionaryToQueryString(Dictionary queryDictionary) + { + var newQueryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString("", queryDictionary); + + return new Microsoft.AspNetCore.Http.QueryString(newQueryString); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs b/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs new file mode 100644 index 00000000..6fa1b8da --- /dev/null +++ b/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.QueryStrings +{ + public interface IAddQueriesToRequest + { + Response SetQueriesOnContext(List claimsToThings, + HttpContext context); + } +} diff --git a/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs b/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs new file mode 100644 index 00000000..1424d713 --- /dev/null +++ b/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.QueryStrings.Middleware +{ + public class QueryStringBuilderMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IAddQueriesToRequest _addQueriesToRequest; + private readonly IOcelotLogger _logger; + + public QueryStringBuilderMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository, + IAddQueriesToRequest addQueriesToRequest) + : base(requestScopedDataRepository) + { + _next = next; + _addQueriesToRequest = addQueriesToRequest; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started calling query string builder middleware"); + + if (DownstreamRoute.ReRoute.ClaimsToQueries.Any()) + { + _logger.LogDebug("this route has instructions to convert claims to queries"); + + var response = _addQueriesToRequest.SetQueriesOnContext(DownstreamRoute.ReRoute.ClaimsToQueries, context); + + if (response.IsError) + { + _logger.LogDebug("there was an error setting queries on context, setting pipeline error"); + + SetPipelineError(response.Errors); + return; + } + } + + _logger.LogDebug("calling next middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + } + } +} diff --git a/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddlewareExtensions.cs b/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddlewareExtensions.cs new file mode 100644 index 00000000..8ff39be2 --- /dev/null +++ b/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.QueryStrings.Middleware +{ + public static class QueryStringBuilderMiddlewareExtensions + { + public static IApplicationBuilder UseQueryStringBuilderMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Request/Builder/HttpRequestCreator.cs b/src/Ocelot/Request/Builder/HttpRequestCreator.cs new file mode 100644 index 00000000..91234331 --- /dev/null +++ b/src/Ocelot/Request/Builder/HttpRequestCreator.cs @@ -0,0 +1,34 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.Responses; + +namespace Ocelot.Request.Builder +{ + public sealed class HttpRequestCreator : IRequestCreator + { + public async Task> Build( + string httpMethod, + string downstreamUrl, + Stream content, + IHeaderDictionary headers, + IRequestCookieCollection cookies, + QueryString queryString, + string contentType, + RequestId.RequestId requestId) + { + var request = await new RequestBuilder() + .WithHttpMethod(httpMethod) + .WithDownstreamUrl(downstreamUrl) + .WithQueryString(queryString) + .WithContent(content) + .WithContentType(contentType) + .WithHeaders(headers) + .WithRequestId(requestId) + .WithCookies(cookies) + .Build(); + + return new OkResponse(request); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Request/Builder/IRequestCreator.cs b/src/Ocelot/Request/Builder/IRequestCreator.cs new file mode 100644 index 00000000..7641d848 --- /dev/null +++ b/src/Ocelot/Request/Builder/IRequestCreator.cs @@ -0,0 +1,19 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.Responses; + +namespace Ocelot.Request.Builder +{ + public interface IRequestCreator + { + Task> Build(string httpMethod, + string downstreamUrl, + Stream content, + IHeaderDictionary headers, + IRequestCookieCollection cookies, + QueryString queryString, + string contentType, + RequestId.RequestId requestId); + } +} diff --git a/src/Ocelot/Request/Builder/RequestBuilder.cs b/src/Ocelot/Request/Builder/RequestBuilder.cs new file mode 100644 index 00000000..c9463993 --- /dev/null +++ b/src/Ocelot/Request/Builder/RequestBuilder.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Ocelot.Request.Builder +{ + internal sealed class RequestBuilder + { + private HttpMethod _method; + private string _downstreamUrl; + private QueryString _queryString; + private Stream _content; + private string _contentType; + private IHeaderDictionary _headers; + private RequestId.RequestId _requestId; + private IRequestCookieCollection _cookies; + private readonly string[] _unsupportedHeaders = {"host"}; + + public RequestBuilder WithHttpMethod(string httpMethod) + { + _method = new HttpMethod(httpMethod); + return this; + } + + public RequestBuilder WithDownstreamUrl(string downstreamUrl) + { + _downstreamUrl = downstreamUrl; + return this; + } + + public RequestBuilder WithQueryString(QueryString queryString) + { + _queryString = queryString; + return this; + } + + public RequestBuilder WithContent(Stream content) + { + _content = content; + return this; + } + + public RequestBuilder WithContentType(string contentType) + { + _contentType = contentType; + return this; + } + + public RequestBuilder WithHeaders(IHeaderDictionary headers) + { + _headers = headers; + return this; + } + + public RequestBuilder WithRequestId(RequestId.RequestId requestId) + { + _requestId = requestId; + return this; + } + + public RequestBuilder WithCookies(IRequestCookieCollection cookies) + { + _cookies = cookies; + return this; + } + + public async Task Build() + { + var uri = CreateUri(); + + var httpRequestMessage = new HttpRequestMessage(_method, uri); + + await AddContentToRequest(httpRequestMessage); + + AddContentTypeToRequest(httpRequestMessage); + + AddHeadersToRequest(httpRequestMessage); + + if (ShouldAddRequestId(_requestId, httpRequestMessage.Headers)) + { + AddRequestIdHeader(_requestId, httpRequestMessage); + } + + var cookieContainer = CreateCookieContainer(uri); + + return new Request(httpRequestMessage, cookieContainer); + } + + private Uri CreateUri() + { + var uri = new Uri(string.Format("{0}{1}", _downstreamUrl, _queryString.ToUriComponent())); + return uri; + } + + private async Task AddContentToRequest(HttpRequestMessage httpRequestMessage) + { + if (_content != null) + { + httpRequestMessage.Content = new ByteArrayContent(await ToByteArray(_content)); + } + } + + private void AddContentTypeToRequest(HttpRequestMessage httpRequestMessage) + { + if (!string.IsNullOrEmpty(_contentType)) + { + httpRequestMessage.Content.Headers.Remove("Content-Type"); + httpRequestMessage.Content.Headers.TryAddWithoutValidation("Content-Type", _contentType); + } + } + + private void AddHeadersToRequest(HttpRequestMessage httpRequestMessage) + { + if (_headers != null) + { + _headers.Remove("Content-Type"); + + foreach (var header in _headers) + { + //todo get rid of if.. + if (IsSupportedHeader(header)) + { + httpRequestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); + } + } + } + } + + private bool IsSupportedHeader(KeyValuePair header) + { + return !_unsupportedHeaders.Contains(header.Key.ToLower()); + } + + private CookieContainer CreateCookieContainer(Uri uri) + { + var cookieContainer = new CookieContainer(); + + if (_cookies != null) + { + foreach (var cookie in _cookies) + { + cookieContainer.Add(uri, new Cookie(cookie.Key, cookie.Value)); + } + } + + return cookieContainer; + } + + private void AddRequestIdHeader(RequestId.RequestId requestId, HttpRequestMessage httpRequestMessage) + { + httpRequestMessage.Headers.Add(requestId.RequestIdKey, requestId.RequestIdValue); + } + + private bool RequestIdInHeaders(RequestId.RequestId requestId, HttpRequestHeaders headers) + { + IEnumerable value; + return headers.TryGetValues(requestId.RequestIdKey, out value); + } + + private bool ShouldAddRequestId(RequestId.RequestId requestId, HttpRequestHeaders headers) + { + return !string.IsNullOrEmpty(requestId?.RequestIdKey) + && !string.IsNullOrEmpty(requestId.RequestIdValue) + && !RequestIdInHeaders(requestId, headers); + } + + private async Task ToByteArray(Stream stream) + { + using (stream) + { + using (var memStream = new MemoryStream()) + { + await stream.CopyToAsync(memStream); + return memStream.ToArray(); + } + } + } + } +} diff --git a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs new file mode 100644 index 00000000..a2c5194b --- /dev/null +++ b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddleware.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Request.Builder; + +namespace Ocelot.Request.Middleware +{ + public class HttpRequestBuilderMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IRequestCreator _requestCreator; + private readonly IOcelotLogger _logger; + + public HttpRequestBuilderMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository, + IRequestCreator requestCreator) + :base(requestScopedDataRepository) + { + _next = next; + _requestCreator = requestCreator; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started calling request builder middleware"); + + var buildResult = await _requestCreator + .Build(context.Request.Method, DownstreamUrl, context.Request.Body, + context.Request.Headers, context.Request.Cookies, context.Request.QueryString, + context.Request.ContentType, new RequestId.RequestId(DownstreamRoute?.ReRoute?.RequestIdKey, context.TraceIdentifier)); + + if (buildResult.IsError) + { + _logger.LogDebug("IRequestCreator returned an error, setting pipeline error"); + + SetPipelineError(buildResult.Errors); + return; + } + _logger.LogDebug("setting upstream request"); + + SetUpstreamRequestForThisRequest(buildResult.Data); + + _logger.LogDebug("calling next middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddlewareExtensions.cs b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddlewareExtensions.cs new file mode 100644 index 00000000..4c08afec --- /dev/null +++ b/src/Ocelot/Request/Middleware/HttpRequestBuilderMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Request.Middleware +{ + public static class HttpRequestBuilderMiddlewareExtensions + { + public static IApplicationBuilder UseHttpRequestBuilderMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Request/Request.cs b/src/Ocelot/Request/Request.cs new file mode 100644 index 00000000..d43071e1 --- /dev/null +++ b/src/Ocelot/Request/Request.cs @@ -0,0 +1,17 @@ +using System.Net; +using System.Net.Http; + +namespace Ocelot.Request +{ + public class Request + { + public Request(HttpRequestMessage httpRequestMessage, CookieContainer cookieContainer) + { + HttpRequestMessage = httpRequestMessage; + CookieContainer = cookieContainer; + } + + public HttpRequestMessage HttpRequestMessage { get; private set; } + public CookieContainer CookieContainer { get; private set; } + } +} diff --git a/src/Ocelot/RequestId/DefaultRequestIdKey.cs b/src/Ocelot/RequestId/DefaultRequestIdKey.cs new file mode 100644 index 00000000..94c0d82d --- /dev/null +++ b/src/Ocelot/RequestId/DefaultRequestIdKey.cs @@ -0,0 +1,9 @@ +namespace Ocelot.RequestId +{ + public static class DefaultRequestIdKey + { + // This is set incase anyone isnt doing this specifically with there requests. + // It will not be forwarded on to downstream services unless specfied in the config. + public const string Value = "RequestId"; + } +} diff --git a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs new file mode 100644 index 00000000..21667351 --- /dev/null +++ b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs @@ -0,0 +1,61 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.RequestId.Middleware +{ + public class RequestIdMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IOcelotLogger _logger; + private readonly IRequestScopedDataRepository _requestScopedDataRepository; + + public RequestIdMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository) + :base(requestScopedDataRepository) + { + _next = next; + _logger = loggerFactory.CreateLogger(); + _requestScopedDataRepository = requestScopedDataRepository; + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started calling request id middleware"); + + SetOcelotRequestId(context); + + _logger.LogDebug("set request id"); + + _logger.LogDebug("calling next middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("succesfully called next middleware"); + } + + private void SetOcelotRequestId(HttpContext context) + { + var key = DefaultRequestIdKey.Value; + + if (DownstreamRoute.ReRoute.RequestIdKey != null) + { + key = DownstreamRoute.ReRoute.RequestIdKey; + } + + StringValues requestId; + + if (context.Request.Headers.TryGetValue(key, out requestId)) + { + _requestScopedDataRepository.Add("RequestId", requestId.First()); + + context.TraceIdentifier = requestId; + } + } + } +} \ No newline at end of file diff --git a/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs b/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs new file mode 100644 index 00000000..dc29afde --- /dev/null +++ b/src/Ocelot/RequestId/Middleware/RequestIdMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.RequestId.Middleware +{ + public static class RequestIdMiddlewareExtensions + { + public static IApplicationBuilder UseRequestIdMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/RequestId/RequestId.cs b/src/Ocelot/RequestId/RequestId.cs new file mode 100644 index 00000000..998d33aa --- /dev/null +++ b/src/Ocelot/RequestId/RequestId.cs @@ -0,0 +1,14 @@ +namespace Ocelot.RequestId +{ + public class RequestId + { + public RequestId(string requestIdKey, string requestIdValue) + { + RequestIdKey = requestIdKey; + RequestIdValue = requestIdValue; + } + + public string RequestIdKey { get; private set; } + public string RequestIdValue { get; private set; } + } +} diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs new file mode 100644 index 00000000..0167ef6a --- /dev/null +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.Requester +{ + public class HttpClientHttpRequester : IHttpRequester + { + public async Task> GetResponse(Request.Request request) + { + using (var handler = new HttpClientHandler { CookieContainer = request.CookieContainer }) + using (var httpClient = new HttpClient(handler)) + { + try + { + var response = await httpClient.SendAsync(request.HttpRequestMessage); + return new OkResponse(response); + } + catch (Exception exception) + { + return + new ErrorResponse(new List + { + new UnableToCompleteRequestError(exception) + }); + } + } + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Requester/IHttpRequester.cs b/src/Ocelot/Requester/IHttpRequester.cs new file mode 100644 index 00000000..201c3add --- /dev/null +++ b/src/Ocelot/Requester/IHttpRequester.cs @@ -0,0 +1,11 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Ocelot.Responses; + +namespace Ocelot.Requester +{ + public interface IHttpRequester + { + Task> GetResponse(Request.Request request); + } +} diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs new file mode 100644 index 00000000..f804569b --- /dev/null +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Requester.Middleware +{ + public class HttpRequesterMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IHttpRequester _requester; + private readonly IOcelotLogger _logger; + + public HttpRequesterMiddleware(RequestDelegate next, + IOcelotLoggerFactory loggerFactory, + IHttpRequester requester, + IRequestScopedDataRepository requestScopedDataRepository) + :base(requestScopedDataRepository) + { + _next = next; + _requester = requester; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started calling requester middleware"); + + var response = await _requester.GetResponse(Request); + + if (response.IsError) + { + _logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); + + SetPipelineError(response.Errors); + return; + } + + _logger.LogDebug("setting http response message"); + + SetHttpResponseMessageThisRequest(response.Data); + + _logger.LogDebug("returning to calling middleware"); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddlewareExtensions.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddlewareExtensions.cs new file mode 100644 index 00000000..6a3d5bb8 --- /dev/null +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Requester.Middleware +{ + public static class HttpRequesterMiddlewareExtensions + { + public static IApplicationBuilder UseHttpRequesterMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Requester/UnableToCompleteRequestError.cs b/src/Ocelot/Requester/UnableToCompleteRequestError.cs new file mode 100644 index 00000000..d085cd1c --- /dev/null +++ b/src/Ocelot/Requester/UnableToCompleteRequestError.cs @@ -0,0 +1,13 @@ +using System; +using Ocelot.Errors; + +namespace Ocelot.Requester +{ + public class UnableToCompleteRequestError : Error + { + public UnableToCompleteRequestError(Exception exception) + : base($"Error making http request, exception: {exception.Message}", OcelotErrorCode.UnableToCompleteRequestError) + { + } + } +} diff --git a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs new file mode 100644 index 00000000..6ecc20eb --- /dev/null +++ b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.Responder +{ + public class ErrorsToHttpStatusCodeMapper : IErrorsToHttpStatusCodeMapper + { + public Response Map(List errors) + { + if (errors.Any(e => e.Code == OcelotErrorCode.UnauthenticatedError)) + { + return new OkResponse(401); + } + + if (errors.Any(e => e.Code == OcelotErrorCode.UnauthorizedError + || e.Code == OcelotErrorCode.ClaimValueNotAuthorisedError + || e.Code == OcelotErrorCode.UserDoesNotHaveClaimError + || e.Code == OcelotErrorCode.CannotFindClaimError)) + { + return new OkResponse(403); + } + + return new OkResponse(404); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Responder/HttpContextResponder.cs b/src/Ocelot/Responder/HttpContextResponder.cs new file mode 100644 index 00000000..8b61e13b --- /dev/null +++ b/src/Ocelot/Responder/HttpContextResponder.cs @@ -0,0 +1,80 @@ +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Ocelot.Headers; +using Ocelot.Responses; + +namespace Ocelot.Responder +{ + using System.Collections.Generic; + + /// + /// Cannot unit test things in this class due to methods not being implemented + /// on .net concretes used for testing + /// + public class HttpContextResponder : IHttpResponder + { + private readonly IRemoveOutputHeaders _removeOutputHeaders; + + public HttpContextResponder(IRemoveOutputHeaders removeOutputHeaders) + { + _removeOutputHeaders = removeOutputHeaders; + } + + public async Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response) + { + _removeOutputHeaders.Remove(response.Headers); + + foreach (var httpResponseHeader in response.Headers) + { + AddHeaderIfDoesntExist(context, httpResponseHeader); + } + + foreach (var httpResponseHeader in response.Content.Headers) + { + AddHeaderIfDoesntExist(context, httpResponseHeader); + } + + var content = await response.Content.ReadAsByteArrayAsync(); + + AddHeaderIfDoesntExist(context, new KeyValuePair>("Content-Length", new []{ content.Length.ToString() }) ); + + context.Response.OnStarting(state => + { + var httpContext = (HttpContext)state; + + httpContext.Response.StatusCode = (int)response.StatusCode; + + return Task.CompletedTask; + + }, context); + + using (Stream stream = new MemoryStream(content)) + { + await stream.CopyToAsync(context.Response.Body); + } + return new OkResponse(); + } + + private static void AddHeaderIfDoesntExist(HttpContext context, KeyValuePair> httpResponseHeader) + { + if (!context.Response.Headers.ContainsKey(httpResponseHeader.Key)) + { + context.Response.Headers.Add(httpResponseHeader.Key, new StringValues(httpResponseHeader.Value.ToArray())); + } + } + + public async Task SetErrorResponseOnContext(HttpContext context, int statusCode) + { + context.Response.OnStarting(x => + { + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + }, context); + return new OkResponse(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Responder/IErrorsToHttpStatusCodeMapper.cs b/src/Ocelot/Responder/IErrorsToHttpStatusCodeMapper.cs new file mode 100644 index 00000000..b4b610d9 --- /dev/null +++ b/src/Ocelot/Responder/IErrorsToHttpStatusCodeMapper.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Ocelot.Errors; +using Ocelot.Responses; + +namespace Ocelot.Responder +{ + public interface IErrorsToHttpStatusCodeMapper + { + Response Map(List errors); + } +} diff --git a/src/Ocelot/Responder/IHttpResponder.cs b/src/Ocelot/Responder/IHttpResponder.cs new file mode 100644 index 00000000..5292f4df --- /dev/null +++ b/src/Ocelot/Responder/IHttpResponder.cs @@ -0,0 +1,13 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.Responses; + +namespace Ocelot.Responder +{ + public interface IHttpResponder + { + Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response); + Task SetErrorResponseOnContext(HttpContext context, int statusCode); + } +} diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs new file mode 100644 index 00000000..06da92dc --- /dev/null +++ b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Errors; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Responder.Middleware +{ + public class ResponderMiddleware : OcelotMiddleware + { + private readonly RequestDelegate _next; + private readonly IHttpResponder _responder; + private readonly IErrorsToHttpStatusCodeMapper _codeMapper; + private readonly IOcelotLogger _logger; + + public ResponderMiddleware(RequestDelegate next, + IHttpResponder responder, + IOcelotLoggerFactory loggerFactory, + IRequestScopedDataRepository requestScopedDataRepository, + IErrorsToHttpStatusCodeMapper codeMapper) + :base(requestScopedDataRepository) + { + _next = next; + _responder = responder; + _codeMapper = codeMapper; + _logger = loggerFactory.CreateLogger(); + + } + + public async Task Invoke(HttpContext context) + { + _logger.LogDebug("started error responder middleware"); + + await _next.Invoke(context); + + _logger.LogDebug("calling next middleware"); + + if (PipelineError) + { + _logger.LogDebug("there is a pipeline error, getting errors"); + + var errors = PipelineErrors; + + _logger.LogDebug("received errors setting error response"); + + await SetErrorResponse(context, errors); + } + else + { + _logger.LogDebug("no pipeline error, setting response"); + + var setResponse = await _responder.SetResponseOnHttpContext(context, HttpResponseMessage); + + if (setResponse.IsError) + { + _logger.LogDebug("error setting response, returning error to client"); + + await SetErrorResponse(context, setResponse.Errors); + } + } + } + + private async Task SetErrorResponse(HttpContext context, List errors) + { + var statusCode = _codeMapper.Map(errors); + + if (!statusCode.IsError) + { + await _responder.SetErrorResponseOnContext(context, statusCode.Data); + } + else + { + await _responder.SetErrorResponseOnContext(context, 500); + } + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddlewareExtensions.cs b/src/Ocelot/Responder/Middleware/ResponderMiddlewareExtensions.cs new file mode 100644 index 00000000..5c119e69 --- /dev/null +++ b/src/Ocelot/Responder/Middleware/ResponderMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Builder; + +namespace Ocelot.Responder.Middleware +{ + public static class ResponderMiddlewareExtensions + { + public static IApplicationBuilder UseResponderMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Responses/ErrorResponse.cs b/src/Ocelot/Responses/ErrorResponse.cs new file mode 100644 index 00000000..9f541507 --- /dev/null +++ b/src/Ocelot/Responses/ErrorResponse.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Ocelot.Errors; + +namespace Ocelot.Responses +{ + public class ErrorResponse : Response + { + public ErrorResponse(List errors) : base(errors) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Responses/ErrorResponseGeneric.cs b/src/Ocelot/Responses/ErrorResponseGeneric.cs new file mode 100644 index 00000000..77230503 --- /dev/null +++ b/src/Ocelot/Responses/ErrorResponseGeneric.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Ocelot.Errors; + +namespace Ocelot.Responses +{ + public class ErrorResponse : Response + { + public ErrorResponse(List errors) : base(errors) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Responses/OkResponse.cs b/src/Ocelot/Responses/OkResponse.cs new file mode 100644 index 00000000..541d3061 --- /dev/null +++ b/src/Ocelot/Responses/OkResponse.cs @@ -0,0 +1,9 @@ +namespace Ocelot.Responses +{ + public class OkResponse : Response + { + public OkResponse() + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Responses/OkResponseGeneric.cs b/src/Ocelot/Responses/OkResponseGeneric.cs new file mode 100644 index 00000000..c979dc91 --- /dev/null +++ b/src/Ocelot/Responses/OkResponseGeneric.cs @@ -0,0 +1,9 @@ +namespace Ocelot.Responses +{ + public class OkResponse : Response + { + public OkResponse(T data) : base(data) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Responses/Response.cs b/src/Ocelot/Responses/Response.cs new file mode 100644 index 00000000..32ff9902 --- /dev/null +++ b/src/Ocelot/Responses/Response.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Ocelot.Errors; + +namespace Ocelot.Responses +{ + public abstract class Response + { + protected Response() + { + Errors = new List(); + } + + protected Response(List errors) + { + Errors = errors ?? new List(); + } + + public List Errors { get; private set; } + + public bool IsError + { + get + { + return Errors.Count > 0; + } + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Responses/ResponseGeneric.cs b/src/Ocelot/Responses/ResponseGeneric.cs new file mode 100644 index 00000000..1902b3e1 --- /dev/null +++ b/src/Ocelot/Responses/ResponseGeneric.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Ocelot.Errors; + +namespace Ocelot.Responses +{ + public abstract class Response : Response + { + protected Response(T data) + { + Data = data; + } + + protected Response(List errors) : base(errors) + { + } + + public T Data { get; private set; } + + } +} \ No newline at end of file diff --git a/src/Ocelot/Utilities/StringExtensions.cs b/src/Ocelot/Utilities/StringExtensions.cs new file mode 100644 index 00000000..4655a98a --- /dev/null +++ b/src/Ocelot/Utilities/StringExtensions.cs @@ -0,0 +1,17 @@ +namespace Ocelot.Utilities +{ + public static class StringExtensions + { + public static string SetLastCharacterAs(this string valueToSetLastChar, + char expectedLastChar) + { + var last = valueToSetLastChar[valueToSetLastChar.Length - 1]; + + if (last != expectedLastChar) + { + valueToSetLastChar = $"{valueToSetLastChar}{expectedLastChar}"; + } + return valueToSetLastChar; + } + } +} diff --git a/src/Ocelot/Values/DownstreamPath.cs b/src/Ocelot/Values/DownstreamPath.cs new file mode 100644 index 00000000..90f2e83e --- /dev/null +++ b/src/Ocelot/Values/DownstreamPath.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Values +{ + public class DownstreamPath + { + public DownstreamPath(string value) + { + Value = value; + } + + public string Value { get; private set; } + } +} diff --git a/src/Ocelot/Values/DownstreamPathTemplate.cs b/src/Ocelot/Values/DownstreamPathTemplate.cs new file mode 100644 index 00000000..a4c720eb --- /dev/null +++ b/src/Ocelot/Values/DownstreamPathTemplate.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Values +{ + public class DownstreamPathTemplate + { + public DownstreamPathTemplate(string value) + { + Value = value; + } + + public string Value { get; private set; } + } +} diff --git a/src/Ocelot/Values/DownstreamUrl.cs b/src/Ocelot/Values/DownstreamUrl.cs new file mode 100644 index 00000000..f809c84b --- /dev/null +++ b/src/Ocelot/Values/DownstreamUrl.cs @@ -0,0 +1,12 @@ +namespace Ocelot.Values +{ + public class DownstreamUrl + { + public DownstreamUrl(string value) + { + Value = value; + } + + public string Value { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/Values/HostAndPort.cs b/src/Ocelot/Values/HostAndPort.cs new file mode 100644 index 00000000..cd336dec --- /dev/null +++ b/src/Ocelot/Values/HostAndPort.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Values +{ + public class HostAndPort + { + public HostAndPort(string downstreamHost, int downstreamPort) + { + DownstreamHost = downstreamHost; + DownstreamPort = downstreamPort; + } + + public string DownstreamHost { get; private set; } + public int DownstreamPort { get; private set; } + } +} \ No newline at end of file diff --git a/src/Ocelot/project.json b/src/Ocelot/project.json new file mode 100644 index 00000000..8d259469 --- /dev/null +++ b/src/Ocelot/project.json @@ -0,0 +1,43 @@ +{ + "version": "0.0.0-dev", + + "dependencies": { + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "System.Text.RegularExpressions": "4.3.0", + "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", + "Microsoft.AspNetCore.Authentication.JwtBearer": "1.1.0", + "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.1.0", + "Microsoft.AspNetCore.Authentication.Cookies": "1.1.0", + "Microsoft.AspNetCore.Authentication.Google": "1.1.0", + "Microsoft.AspNetCore.Authentication.Facebook": "1.1.0", + "Microsoft.AspNetCore.Authentication.Twitter": "1.1.0", + "Microsoft.AspNetCore.Authentication.MicrosoftAccount": "1.1.0", + "Microsoft.AspNetCore.Authentication": "1.1.0", + "IdentityServer4.AccessTokenValidation": "1.0.2", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "CacheManager.Core": "0.9.2", + "CacheManager.Microsoft.Extensions.Configuration": "0.9.2", + "CacheManager.Microsoft.Extensions.Logging": "0.9.2" + }, + "runtimes": { + "win10-x64": {}, + "osx.10.11-x64":{}, + "win7-x64": {} + }, + "frameworks": { + "netcoreapp1.4": { + "imports": [ + ] + } + } +} diff --git a/src/Ocelot.ApiGateway/.gitignore b/test/Ocelot.AcceptanceTests/.gitignore similarity index 100% rename from src/Ocelot.ApiGateway/.gitignore rename to test/Ocelot.AcceptanceTests/.gitignore diff --git a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs new file mode 100644 index 00000000..8b14f4f1 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs @@ -0,0 +1,340 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Security.Claims; +using IdentityServer4.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + using IdentityServer4; + using IdentityServer4.Test; + + public class AuthenticationTests : IDisposable + { + private IWebHost _servicebuilder; + private readonly Steps _steps; + private IWebHost _identityServerBuilder; + private string _identityServerRootUrl = "http://localhost:51888"; + private string _downstreamServicePath = "/"; + private string _downstreamServiceHost = "localhost"; + private int _downstreamServicePort = 51876; + private string _downstreamServiceScheme = "http"; + private string _downstreamServiceUrl = "http://localhost:51876"; + + public AuthenticationTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_401_using_identity_server_access_token() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = _downstreamServicePath, + DownstreamPort = _downstreamServicePort, + DownstreamHost = _downstreamServiceHost, + DownstreamScheme = _downstreamServiceScheme, + UpstreamTemplate = "/", + UpstreamHttpMethod = "Post", + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List(), + Provider = "IdentityServer", + ProviderRootUrl = _identityServerRootUrl, + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret" + } + } + } + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt)) + .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceUrl, 201, string.Empty)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) + .BDDfy(); + } + + [Fact] + public void should_return_401_using_identity_server_reference_token() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = _downstreamServicePath, + DownstreamPort = _downstreamServicePort, + DownstreamHost = _downstreamServiceHost, + DownstreamScheme = _downstreamServiceScheme, + UpstreamTemplate = "/", + UpstreamHttpMethod = "Post", + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List(), + Provider = "IdentityServer", + ProviderRootUrl = _identityServerRootUrl, + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret" + } + } + } + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Reference)) + .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceUrl, 201, string.Empty)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_using_identity_server() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = _downstreamServicePath, + DownstreamPort = _downstreamServicePort, + DownstreamHost = _downstreamServiceHost, + DownstreamScheme = _downstreamServiceScheme, + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List(), + Provider = "IdentityServer", + ProviderRootUrl = _identityServerRootUrl, + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret" + } + } + } + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt)) + .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceUrl, 200, "Hello from Laura")) + .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_201_using_identity_server_access_token() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = _downstreamServicePath, + DownstreamPort = _downstreamServicePort, + DownstreamHost = _downstreamServiceHost, + DownstreamScheme = _downstreamServiceScheme, + UpstreamTemplate = "/", + UpstreamHttpMethod = "Post", + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List(), + Provider = "IdentityServer", + ProviderRootUrl = _identityServerRootUrl, + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret" + } + } + } + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Jwt)) + .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceUrl, 201, string.Empty)) + .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .BDDfy(); + } + + [Fact] + public void should_return_201_using_identity_server_reference_token() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = _downstreamServicePath, + DownstreamPort = _downstreamServicePort, + DownstreamHost = _downstreamServiceHost, + DownstreamScheme = _downstreamServiceScheme, + UpstreamTemplate = "/", + UpstreamHttpMethod = "Post", + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List(), + Provider = "IdentityServer", + ProviderRootUrl = _identityServerRootUrl, + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret" + } + } + } + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn(_identityServerRootUrl, "api", AccessTokenType.Reference)) + .And(x => x.GivenThereIsAServiceRunningOn(_downstreamServiceUrl, 201, string.Empty)) + .And(x => _steps.GivenIHaveAToken(_identityServerRootUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + { + _servicebuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _servicebuilder.Start(); + } + + private void GivenThereIsAnIdentityServerOn(string url, string scopeName, AccessTokenType tokenType) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddTemporarySigningCredential() + .AddInMemoryApiResources(new List + { + new ApiResource + { + Name = scopeName, + Description = "My API", + Enabled = true, + DisplayName = "test", + Scopes = new List() + { + new Scope("api"), + new Scope("openid"), + new Scope("offline_access") + }, + ApiSecrets = new List() + { + new Secret + { + Value = "secret".Sha256() + } + }, + UserClaims = new List() + { + "CustomerId", "LocationId" + } + }, + }) + .AddInMemoryClients(new List + { + new Client + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new Secret("secret".Sha256())}, + AllowedScopes = new List { scopeName, "openid", "offline_access" }, + AccessTokenType = tokenType, + Enabled = true, + RequireClientSecret = false + } + }) + .AddTestUsers(new List + { + new TestUser + { + Username = "test", + Password = "test", + SubjectId = "registered|1231231", + Claims = new List + { + new Claim("CustomerId", "123"), + new Claim("LocationId", "321") + } + } + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + _steps.VerifyIdentiryServerStarted(url); + } + + public void Dispose() + { + _servicebuilder?.Dispose(); + _steps.Dispose(); + _identityServerBuilder?.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/AuthorisationTests.cs b/test/Ocelot.AcceptanceTests/AuthorisationTests.cs new file mode 100644 index 00000000..1f86c6ff --- /dev/null +++ b/test/Ocelot.AcceptanceTests/AuthorisationTests.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Security.Claims; +using IdentityServer4.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + using IdentityServer4; + using IdentityServer4.Test; + + public class AuthorisationTests : IDisposable + { + private IWebHost _servicebuilder; + private IWebHost _identityServerBuilder; + private readonly Steps _steps; + + public AuthorisationTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_authorising_route() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 51876, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List(), + Provider = "IdentityServer", + ProviderRootUrl = "http://localhost:51888", + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret" + }, + AddHeadersToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"LocationId", "Claims[LocationId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"} + }, + AddClaimsToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"} + }, + RouteClaimsRequirement = + { + {"UserType", "registered"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888", "api", AccessTokenType.Jwt)) + .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", 200, "Hello from Laura")) + .And(x => _steps.GivenIHaveAToken("http://localhost:51888")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_403_authorising_route() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 51876, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List(), + Provider = "IdentityServer", + ProviderRootUrl = "http://localhost:51888", + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret" + }, + AddHeadersToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"LocationId", "Claims[LocationId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"} + }, + AddClaimsToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"UserId", "Claims[sub] > value[1] > |"} + }, + RouteClaimsRequirement = + { + {"UserType", "registered"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:51888", "api", AccessTokenType.Jwt)) + .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51876", 200, "Hello from Laura")) + .And(x => _steps.GivenIHaveAToken("http://localhost:51888")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + { + _servicebuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _servicebuilder.Start(); + } + + private void GivenThereIsAnIdentityServerOn(string url, string scopeName, AccessTokenType tokenType) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddTemporarySigningCredential() + .AddInMemoryApiResources(new List + { + new ApiResource + { + Name = scopeName, + Description = "My API", + Enabled = true, + DisplayName = "test", + Scopes = new List() + { + new Scope("api"), + new Scope("openid"), + new Scope("offline_access") + }, + ApiSecrets = new List() + { + new Secret + { + Value = "secret".Sha256() + } + }, + UserClaims = new List() + { + "CustomerId", "LocationId", "UserType", "UserId" + } + }, + + }) + .AddInMemoryClients(new List + { + new Client + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new Secret("secret".Sha256())}, + AllowedScopes = new List { scopeName, "openid", "offline_access" }, + AccessTokenType = tokenType, + Enabled = true, + RequireClientSecret = false + } + }) + .AddTestUsers(new List + { + new TestUser + { + Username = "test", + Password = "test", + SubjectId = "registered|1231231", + Claims = new List + { + new Claim("CustomerId", "123"), + new Claim("LocationId", "321") + } + } + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + _steps.VerifyIdentiryServerStarted(url); + + } + + public void Dispose() + { + _servicebuilder?.Dispose(); + _steps.Dispose(); + _identityServerBuilder?.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/BearerToken.cs b/test/Ocelot.AcceptanceTests/BearerToken.cs new file mode 100644 index 00000000..26fd8d3f --- /dev/null +++ b/test/Ocelot.AcceptanceTests/BearerToken.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Ocelot.AcceptanceTests +{ + class BearerToken + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + + [JsonProperty("token_type")] + public string TokenType { get; set; } + } +} \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/CachingTests.cs b/test/Ocelot.AcceptanceTests/CachingTests.cs new file mode 100644 index 00000000..e4e628af --- /dev/null +++ b/test/Ocelot.AcceptanceTests/CachingTests.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class CachingTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + + public CachingTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_cached_response() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 100 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => x.GivenTheServiceNowReturns("http://localhost:51879", 200, "Hello from Tom")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_not_return_cached_response_as_ttl_expires() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + FileCacheOptions = new FileCacheOptions + { + TtlSeconds = 1 + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => x.GivenTheServiceNowReturns("http://localhost:51879", 200, "Hello from Tom")) + .And(x => x.GivenTheCacheExpires()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .BDDfy(); + } + + private void GivenTheCacheExpires() + { + Thread.Sleep(1000); + } + + private void GivenTheServiceNowReturns(string url, int statusCode, string responseBody) + { + _builder.Dispose(); + GivenThereIsAServiceRunningOn(url, statusCode, responseBody); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs new file mode 100644 index 00000000..81824602 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class CaseSensitiveRoutingTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + + public CaseSensitiveRoutingTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_when_global_ignore_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get" + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_reroute_ignore_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = false + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_404_when_reroute_respect_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_reroute_respect_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/PRODUCTS/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_return_response_404_when_global_respect_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_global_respect_case_sensitivity_set() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/PRODUCTS/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs new file mode 100644 index 00000000..08bbd968 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Claims; +using IdentityServer4.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +[assembly: CollectionBehavior(DisableTestParallelization = true)] +namespace Ocelot.AcceptanceTests +{ + using IdentityServer4; + using IdentityServer4.Test; + + public class ClaimsToHeadersForwardingTests : IDisposable + { + private IWebHost _servicebuilder; + private IWebHost _identityServerBuilder; + private readonly Steps _steps; + + public ClaimsToHeadersForwardingTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_and_foward_claim_as_header() + { + var user = new TestUser() + { + Username = "test", + Password = "test", + SubjectId = "registered|1231231", + Claims = new List + { + new Claim("CustomerId", "123"), + new Claim("LocationId", "1") + } + }; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 52876, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List + { + "openid", "offline_access" + }, + Provider = "IdentityServer", + ProviderRootUrl = "http://localhost:52888", + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret", + }, + AddHeadersToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"LocationId", "Claims[LocationId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:52888", "api", AccessTokenType.Jwt, user)) + .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:52876", 200)) + .And(x => _steps.GivenIHaveAToken("http://localhost:52888")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("CustomerId: 123 LocationId: 1 UserType: registered UserId: 1231231")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode) + { + _servicebuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + var customerId = context.Request.Headers.First(x => x.Key == "CustomerId").Value.First(); + var locationId = context.Request.Headers.First(x => x.Key == "LocationId").Value.First(); + var userType = context.Request.Headers.First(x => x.Key == "UserType").Value.First(); + var userId = context.Request.Headers.First(x => x.Key == "UserId").Value.First(); + + var responseBody = $"CustomerId: {customerId} LocationId: {locationId} UserType: {userType} UserId: {userId}"; + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _servicebuilder.Start(); + } + + private void GivenThereIsAnIdentityServerOn(string url, string scopeName, AccessTokenType tokenType, TestUser user) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddTemporarySigningCredential() + .AddInMemoryApiResources(new List + { + new ApiResource + { + Name = scopeName, + Description = "My API", + Enabled = true, + DisplayName = "test", + Scopes = new List() + { + new Scope("api"), + new Scope("openid"), + new Scope("offline_access") + }, + ApiSecrets = new List() + { + new Secret + { + Value = "secret".Sha256() + } + }, + UserClaims = new List() + { + "CustomerId", "LocationId", "UserType", "UserId" + } + } + }) + .AddInMemoryClients(new List + { + new Client + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new Secret("secret".Sha256())}, + AllowedScopes = new List { scopeName, "openid", "offline_access" }, + AccessTokenType = tokenType, + Enabled = true, + RequireClientSecret = false + } + }) + .AddTestUsers(new List + { + user + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + _steps.VerifyIdentiryServerStarted(url); + } + + public void Dispose() + { + _servicebuilder?.Dispose(); + _steps.Dispose(); + _identityServerBuilder?.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs new file mode 100644 index 00000000..04dc25db --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Claims; +using IdentityServer4.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + using IdentityServer4; + using IdentityServer4.Test; + + public class ClaimsToQueryStringForwardingTests : IDisposable + { + private IWebHost _servicebuilder; + private IWebHost _identityServerBuilder; + private readonly Steps _steps; + + public ClaimsToQueryStringForwardingTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_and_foward_claim_as_query_string() + { + var user = new TestUser() + { + Username = "test", + Password = "test", + SubjectId = "registered|1231231", + Claims = new List + { + new Claim("CustomerId", "123"), + new Claim("LocationId", "1") + } + }; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 57876, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List + { + "openid", "offline_access" + }, + Provider = "IdentityServer", + ProviderRootUrl = "http://localhost:57888", + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret", + }, + AddQueriesToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + {"LocationId", "Claims[LocationId] > value"}, + {"UserType", "Claims[sub] > value[0] > |"}, + {"UserId", "Claims[sub] > value[1] > |"} + } + } + } + }; + + this.Given(x => x.GivenThereIsAnIdentityServerOn("http://localhost:57888", "api", AccessTokenType.Jwt, user)) + .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:57876", 200)) + .And(x => _steps.GivenIHaveAToken("http://localhost:57888")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("CustomerId: 123 LocationId: 1 UserType: registered UserId: 1231231")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode) + { + _servicebuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + StringValues customerId; + context.Request.Query.TryGetValue("CustomerId", out customerId); + + StringValues locationId; + context.Request.Query.TryGetValue("LocationId", out locationId); + + StringValues userType; + context.Request.Query.TryGetValue("UserType", out userType); + + StringValues userId; + context.Request.Query.TryGetValue("UserId", out userId); + + var responseBody = $"CustomerId: {customerId} LocationId: {locationId} UserType: {userType} UserId: {userId}"; + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _servicebuilder.Start(); + } + + private void GivenThereIsAnIdentityServerOn(string url, string scopeName, AccessTokenType tokenType, TestUser user) + { + _identityServerBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .ConfigureServices(services => + { + services.AddLogging(); + services.AddIdentityServer() + .AddTemporarySigningCredential() + .AddInMemoryApiResources(new List + { + new ApiResource + { + Name = scopeName, + Description = "My API", + Enabled = true, + DisplayName = "test", + Scopes = new List() + { + new Scope("api"), + new Scope("openid"), + new Scope("offline_access") + }, + ApiSecrets = new List() + { + new Secret + { + Value = "secret".Sha256() + } + }, + UserClaims = new List() + { + "CustomerId", "LocationId", "UserType", "UserId" + } + } + }) + .AddInMemoryClients(new List + { + new Client + { + ClientId = "client", + AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, + ClientSecrets = new List {new Secret("secret".Sha256())}, + AllowedScopes = new List { scopeName, "openid", "offline_access" }, + AccessTokenType = tokenType, + Enabled = true, + RequireClientSecret = false + } + }) + .AddTestUsers(new List + { + user + }); + }) + .Configure(app => + { + app.UseIdentityServer(); + }) + .Build(); + + _identityServerBuilder.Start(); + + _steps.VerifyIdentiryServerStarted(url); + } + + public void Dispose() + { + _servicebuilder?.Dispose(); + _steps.Dispose(); + _identityServerBuilder?.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs new file mode 100644 index 00000000..f6f6de20 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Ocelot.Configuration.File; +using Ocelot.Middleware; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class CustomMiddlewareTests : IDisposable + { + private readonly string _configurationPath; + private IWebHost _builder; + private readonly Steps _steps; + private int _counter; + + public CustomMiddlewareTests() + { + _counter = 0; + _steps = new Steps();; + _configurationPath = "configuration.json"; + } + + [Fact] + public void should_call_pre_query_string_builder_middleware() + { + var configuration = new OcelotMiddlewareConfiguration + { + AuthorisationMiddleware = async (ctx, next) => + { + _counter++; + await next.Invoke(); + } + }; + + var fileConfiguration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200)) + .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) + .And(x => _steps.GivenOcelotIsRunning(configuration)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => x.ThenTheCounterIs(1)) + .BDDfy(); + } + + [Fact] + public void should_call_authorisation_middleware() + { + var configuration = new OcelotMiddlewareConfiguration + { + AuthorisationMiddleware = async (ctx, next) => + { + _counter++; + await next.Invoke(); + } + }; + + var fileConfiguration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200)) + .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) + .And(x => _steps.GivenOcelotIsRunning(configuration)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => x.ThenTheCounterIs(1)) + .BDDfy(); + } + + [Fact] + public void should_call_authentication_middleware() + { + var configuration = new OcelotMiddlewareConfiguration + { + AuthenticationMiddleware = async (ctx, next) => + { + _counter++; + await next.Invoke(); + } + }; + + var fileConfiguration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "41879/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200)) + .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) + .And(x => _steps.GivenOcelotIsRunning(configuration)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => x.ThenTheCounterIs(1)) + .BDDfy(); + } + + [Fact] + public void should_call_pre_error_middleware() + { + var configuration = new OcelotMiddlewareConfiguration + { + PreErrorResponderMiddleware = async (ctx, next) => + { + _counter++; + await next.Invoke(); + } + }; + + var fileConfiguration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200)) + .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) + .And(x => _steps.GivenOcelotIsRunning(configuration)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => x.ThenTheCounterIs(1)) + .BDDfy(); + } + + [Fact] + public void should_call_pre_authorisation_middleware() + { + var configuration = new OcelotMiddlewareConfiguration + { + PreAuthorisationMiddleware = async (ctx, next) => + { + _counter++; + await next.Invoke(); + } + }; + + var fileConfiguration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200)) + .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) + .And(x => _steps.GivenOcelotIsRunning(configuration)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => x.ThenTheCounterIs(1)) + .BDDfy(); + } + + [Fact] + public void should_call_pre_http_authentication_middleware() + { + var configuration = new OcelotMiddlewareConfiguration + { + PreAuthenticationMiddleware = async (ctx, next) => + { + _counter++; + await next.Invoke(); + } + }; + + var fileConfiguration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 41879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200)) + .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath)) + .And(x => _steps.GivenOcelotIsRunning(configuration)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => x.ThenTheCounterIs(1)) + .BDDfy(); + } + + private void ThenTheCounterIs(int expected) + { + _counter.ShouldBe(expected); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(context => + { + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.xproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.xproj new file mode 100644 index 00000000..84e2327e --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + f8c224fe-36be-45f5-9b0e-666d8f4a9b52 + Ocelot.AcceptanceTests + .\obj + .\bin\ + v4.5 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/Properties/AssemblyInfo.cs b/test/Ocelot.AcceptanceTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..2f3ceca8 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Ocelot.AcceptanceTests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("f8c224fe-36be-45f5-9b0e-666d8f4a9b52")] diff --git a/test/Ocelot.AcceptanceTests/RequestIdTests.cs b/test/Ocelot.AcceptanceTests/RequestIdTests.cs new file mode 100644 index 00000000..9334786b --- /dev/null +++ b/test/Ocelot.AcceptanceTests/RequestIdTests.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class RequestIdTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + + public RequestIdTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_use_default_request_id_and_forward() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + RequestIdKey = _steps.RequestIdKey + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheRequestIdIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_use_request_id_and_forward() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + RequestIdKey = _steps.RequestIdKey + } + } + }; + + var requestId = Guid.NewGuid().ToString(); + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", requestId)) + .Then(x => _steps.ThenTheRequestIdIsReturned(requestId)) + .BDDfy(); + } + + [Fact] + public void should_use_global_request_id_and_forward() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamPort = 51879, + DownstreamScheme = "http", + DownstreamHost = "localhost", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + RequestIdKey = _steps.RequestIdKey + } + }; + + var requestId = Guid.NewGuid().ToString(); + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/", requestId)) + .Then(x => _steps.ThenTheRequestIdIsReturned(requestId)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(context => + { + StringValues requestId; + context.Request.Headers.TryGetValue(_steps.RequestIdKey, out requestId); + context.Response.Headers.Add(_steps.RequestIdKey, requestId.First()); + return Task.CompletedTask; + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs new file mode 100644 index 00000000..81307781 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class ReturnsErrorTests : IDisposable + { + private IWebHost _servicebuilder; + private readonly Steps _steps; + + public ReturnsErrorTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_response_200_and_foward_claim_as_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "http://localhost:53876/", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get" + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:53876")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url) + { + _servicebuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(context => + { + throw new Exception("BLAMMMM"); + }); + }) + .Build(); + + _servicebuilder.Start(); + } + + public void Dispose() + { + _servicebuilder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs new file mode 100644 index 00000000..4f97114f --- /dev/null +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.File; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class RoutingTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + + public RoutingTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_return_response_404_when_no_configuration_at_all() + { + this.Given(x => _steps.GivenThereIsAConfiguration(new FileConfiguration())) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_simple_url() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_path_missing_forward_slash_as_first_char() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "api/products", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_when_host_has_trailing_slash() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products", + DownstreamScheme = "http", + DownstreamHost = "localhost/", + DownstreamPort = 51879, + UpstreamTemplate = "/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_not_care_about_no_trailing() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamTemplate = "/products/", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_not_care_about_trailing() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamTemplate = "/products", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_return_not_found() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/products", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/products", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + [Fact] + public void should_return_response_200_with_complex_url() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/{productId}", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get" + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879/api/products/1", 200, "Some Product")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/products/1")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Some Product")) + .BDDfy(); + } + + [Fact] + public void should_return_response_201_with_simple_url() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHost = "localhost", + DownstreamPort = 51879, + DownstreamScheme = "http", + UpstreamTemplate = "/", + UpstreamHttpMethod = "Post" + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", 201, string.Empty)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) + .BDDfy(); + } + + [Fact] + public void should_return_response_201_with_complex_query_string() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/newThing", + UpstreamTemplate = "/newThing", + DownstreamScheme = "http", + DownstreamHost = "localhost", + DownstreamPort = 51879, + UpstreamHttpMethod = "Get", + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/newThing?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + { + _builder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _builder.Start(); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs new file mode 100644 index 00000000..c2bd7ee7 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using CacheManager.Core; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.ManualTest; +using Ocelot.Middleware; +using Shouldly; +using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; + +namespace Ocelot.AcceptanceTests +{ + public class Steps : IDisposable + { + private TestServer _ocelotServer; + private HttpClient _ocelotClient; + private HttpResponseMessage _response; + private HttpContent _postContent; + private BearerToken _token; + public HttpClient OcelotClient => _ocelotClient; + public string RequestIdKey = "OcRequestId"; + + public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = TestConfiguration.ConfigurationPath; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration, string configurationPath) + { + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + public void GivenOcelotIsRunning() + { + _ocelotServer = new TestServer(new WebHostBuilder() + .UseStartup()); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + public void GivenOcelotIsRunning(OcelotMiddlewareConfiguration ocelotMiddlewareConfig) + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile("configuration.json") + .AddEnvironmentVariables(); + + var configuration = builder.Build(); + + _ocelotServer = new TestServer(new WebHostBuilder() + .UseConfiguration(configuration) + .ConfigureServices(s => + { + Action settings = (x) => + { + x.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; + + s.AddOcelotOutputCaching(settings); + s.AddOcelotFileConfiguration(configuration); + s.AddOcelot(); + }) + .ConfigureLogging(l => + { + l.AddConsole(configuration.GetSection("Logging")); + l.AddDebug(); + }) + .Configure(a => + { + a.UseOcelot(ocelotMiddlewareConfig); + })); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenIHaveAddedATokenToMyRequest() + { + _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } + + public void GivenIHaveAToken(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new KeyValuePair("client_id", "client"), + new KeyValuePair("client_secret", "secret"), + new KeyValuePair("scope", "api"), + new KeyValuePair("username", "test"), + new KeyValuePair("password", "test"), + new KeyValuePair("grant_type", "password") + }; + var content = new FormUrlEncodedContent(formData); + + using (var httpClient = new HttpClient()) + { + var response = httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } + } + + public void VerifyIdentiryServerStarted(string url) + { + using (var httpClient = new HttpClient()) + { + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; + response.EnsureSuccessStatusCode(); + } + } + + + public void WhenIGetUrlOnTheApiGateway(string url) + { + _response = _ocelotClient.GetAsync(url).Result; + } + + public void WhenIGetUrlOnTheApiGateway(string url, string requestId) + { + _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); + + _response = _ocelotClient.GetAsync(url).Result; + } + + public void WhenIPostUrlOnTheApiGateway(string url) + { + _response = _ocelotClient.PostAsync(url, _postContent).Result; + } + + public void GivenThePostHasContent(string postcontent) + { + _postContent = new StringContent(postcontent); + } + + public void ThenTheResponseBodyShouldBe(string expectedBody) + { + _response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody); + } + + public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + { + _response.StatusCode.ShouldBe(expectedHttpStatusCode); + } + + public void Dispose() + { + _ocelotClient?.Dispose(); + _ocelotServer?.Dispose(); + } + + public void ThenTheRequestIdIsReturned() + { + _response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); + } + + public void ThenTheRequestIdIsReturned(string expected) + { + _response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/TestConfiguration.cs b/test/Ocelot.AcceptanceTests/TestConfiguration.cs new file mode 100644 index 00000000..0aa730be --- /dev/null +++ b/test/Ocelot.AcceptanceTests/TestConfiguration.cs @@ -0,0 +1,38 @@ +namespace Ocelot.AcceptanceTests +{ + using System.Runtime.InteropServices; + + public static class TestConfiguration + { + public static double Version => 1.4; + public static string ConfigurationPath => GetConfigurationPath(); + + public static string GetConfigurationPath() + { + var osArchitecture = RuntimeInformation.OSArchitecture.ToString(); + + var oSDescription = string.Empty; + + if(RuntimeInformation.OSDescription.ToLower().Contains("darwin")) + { + return FormatConfigurationPath("osx.10.11", osArchitecture); + } + + if(RuntimeInformation.OSDescription.ToLower().Contains("microsoft windows 10")) + { + return FormatConfigurationPath("win10", osArchitecture); + } + + return FormatConfigurationPath("win7", osArchitecture); + } + + private static string FormatConfigurationPath(string oSDescription, string osArchitecture) + { + var runTime = $"{oSDescription}-{osArchitecture}".ToLower(); + + var configPath = $"./bin/Debug/netcoreapp{Version}/{runTime}/configuration.json"; + + return configPath; + } + } +} diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json new file mode 100755 index 00000000..cdcb1624 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/configuration.json @@ -0,0 +1 @@ +{"ReRoutes":[{"DownstreamPathTemplate":"41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"http","DownstreamHost":"localhost","DownstreamPort":41879}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":null,"Address":null}}} \ No newline at end of file diff --git a/test/Ocelot.AcceptanceTests/project.json b/test/Ocelot.AcceptanceTests/project.json new file mode 100644 index 00000000..17f35a3c --- /dev/null +++ b/test/Ocelot.AcceptanceTests/project.json @@ -0,0 +1,48 @@ +{ + "version": "0.0.0-dev", + + "buildOptions": { + "copyToOutput": { + "include": [ + "configuration.json" + ] + } + }, + + "testRunner": "xunit", + + "dependencies": { + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "Microsoft.DotNet.InternalAbstractions": "1.0.0", + "Ocelot": "0.0.0-dev", + "xunit": "2.2.0-beta2-build3300", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Ocelot.ManualTest": "0.0.0-dev", + "Microsoft.AspNetCore.TestHost": "1.1.0", + "IdentityServer4": "1.0.1", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "Shouldly": "2.8.2", + "TestStack.BDDfy": "4.3.2" + }, + "runtimes": { + "win10-x64": {}, + "osx.10.11-x64": {}, + "win7-x64": {} + }, + "frameworks": { + "netcoreapp1.4": { + "imports": [ + ] + } + } +} diff --git a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.xproj b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.xproj new file mode 100644 index 00000000..9a2d931f --- /dev/null +++ b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 106b49e6-95f6-4a7b-b81c-96bfa74af035 + Ocelot.Benchmarks + .\obj + .\bin\ + v4.6.1 + + + + 2.0 + + + diff --git a/test/Ocelot.Benchmarks/Program.cs b/test/Ocelot.Benchmarks/Program.cs new file mode 100644 index 00000000..66efb040 --- /dev/null +++ b/test/Ocelot.Benchmarks/Program.cs @@ -0,0 +1,17 @@ +using BenchmarkDotNet.Running; + +namespace Ocelot.Benchmarks +{ + public class Program + { + public static void Main(string[] args) + { + var switcher = new BenchmarkSwitcher(new[] { + typeof(UrlPathToUrlPathTemplateMatcherBenchmarks), + + }); + + switcher.Run(args); + } + } +} diff --git a/test/Ocelot.Benchmarks/Properties/AssemblyInfo.cs b/test/Ocelot.Benchmarks/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..b10c2d2f --- /dev/null +++ b/test/Ocelot.Benchmarks/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Ocelot.Benchmarks")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("106b49e6-95f6-4a7b-b81c-96bfa74af035")] diff --git a/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs b/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs new file mode 100644 index 00000000..40dc5036 --- /dev/null +++ b/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs @@ -0,0 +1,40 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using Ocelot.DownstreamRouteFinder.UrlMatcher; + +namespace Ocelot.Benchmarks +{ + [Config(typeof(UrlPathToUrlPathTemplateMatcherBenchmarks))] + public class UrlPathToUrlPathTemplateMatcherBenchmarks : ManualConfig + { + private RegExUrlMatcher _urlPathMatcher; + private string _downstreamUrlPath; + private string _downstreamUrlPathTemplate; + + public UrlPathToUrlPathTemplateMatcherBenchmarks() + { + Add(StatisticColumn.AllStatistics); + } + + [Setup] + public void SetUp() + { + _urlPathMatcher = new RegExUrlMatcher(); + _downstreamUrlPath = "api/product/products/1/variants/?soldout=false"; + _downstreamUrlPathTemplate = "api/product/products/{productId}/variants/"; + } + + [Benchmark] + public void Benchmark1() + { + _urlPathMatcher.Match(_downstreamUrlPath, _downstreamUrlPathTemplate); + } + + [Benchmark] + public void Benchmark2() + { + _urlPathMatcher.Match(_downstreamUrlPath, _downstreamUrlPathTemplate); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.Benchmarks/project.json b/test/Ocelot.Benchmarks/project.json new file mode 100644 index 00000000..5f7a4987 --- /dev/null +++ b/test/Ocelot.Benchmarks/project.json @@ -0,0 +1,22 @@ +{ + "version": "0.0.0-dev", + "buildOptions": { + "emitEntryPoint": true + }, + + "dependencies": { + "Ocelot": "0.0.0-dev", + "BenchmarkDotNet": "0.10.1" + }, + "runtimes": { + "win10-x64": {}, + "osx.10.11-x64":{}, + "win7-x64": {} + }, + "frameworks": { + "netcoreapp1.4": { + "imports": [ + ] + } + } +} diff --git a/test/Ocelot.ManualTest/Ocelot.ManualTest.xproj b/test/Ocelot.ManualTest/Ocelot.ManualTest.xproj new file mode 100644 index 00000000..16a89a50 --- /dev/null +++ b/test/Ocelot.ManualTest/Ocelot.ManualTest.xproj @@ -0,0 +1,25 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 02bbf4c5-517e-4157-8d21-4b8b9e118b7a + Ocelot.ManualTest + .\obj + .\bin\ + v4.6.1 + + + + 2.0 + + + + + + + diff --git a/test/Ocelot.ManualTest/Ocelot.postman_collection.json b/test/Ocelot.ManualTest/Ocelot.postman_collection.json new file mode 100644 index 00000000..eef5f5e8 --- /dev/null +++ b/test/Ocelot.ManualTest/Ocelot.postman_collection.json @@ -0,0 +1,184 @@ +{ + "id": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "name": "Ocelot", + "description": "", + "order": [ + "a1c95935-ed18-d5dc-bcb8-a3db8ba1934f", + "ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2", + "c4494401-3985-a5bf-71fb-6e4171384ac6", + "09af8dda-a9cb-20d2-5ee3-0a3023773a1a", + "e8825dc3-4137-99a7-0000-ef5786610dc3", + "fddfc4fa-5114-69e3-4744-203ed71a526b", + "c45d30d7-d9c4-fa05-8110-d6e769bb6ff9", + "4684c2fa-f38c-c193-5f55-bf563a1978c6" + ], + "folders": [], + "timestamp": 1477767328599, + "owner": "212120", + "public": false, + "requests": [ + { + "id": "09af8dda-a9cb-20d2-5ee3-0a3023773a1a", + "headers": "", + "url": "http://localhost:5000/comments?postId=1", + "pathVariables": {}, + "preRequestScript": null, + "method": "GET", + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "data": null, + "dataMode": "params", + "name": "GET http://localhost:5000/comments?postId=1", + "description": "", + "descriptionFormat": "html", + "time": 1477768105592, + "version": 2, + "responses": [], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {} + }, + { + "id": "4684c2fa-f38c-c193-5f55-bf563a1978c6", + "headers": "", + "url": "http://localhost:5000/posts/1", + "pathVariables": {}, + "preRequestScript": null, + "method": "DELETE", + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "data": null, + "dataMode": "params", + "name": "DELETE http://localhost:5000/posts/1", + "description": "", + "descriptionFormat": "html", + "time": 1477768404376, + "version": 2, + "responses": [], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {} + }, + { + "id": "a1c95935-ed18-d5dc-bcb8-a3db8ba1934f", + "headers": "", + "url": "http://localhost:5000/posts", + "pathVariables": {}, + "preRequestScript": null, + "method": "GET", + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "data": null, + "dataMode": "params", + "name": "GET http://localhost:5000/posts", + "description": "", + "descriptionFormat": "html", + "time": 1477768007806, + "version": 2, + "responses": [], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {} + }, + { + "id": "c4494401-3985-a5bf-71fb-6e4171384ac6", + "headers": "", + "url": "http://localhost:5000/posts/1/comments", + "pathVariables": {}, + "preRequestScript": null, + "method": "GET", + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "data": null, + "dataMode": "params", + "name": "GET http://localhost:5000/posts/1/comments", + "description": "", + "descriptionFormat": "html", + "time": 1477768043524, + "version": 2, + "responses": [], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {} + }, + { + "id": "c45d30d7-d9c4-fa05-8110-d6e769bb6ff9", + "headers": "", + "url": "http://localhost:5000/posts/1", + "pathVariables": {}, + "preRequestScript": null, + "method": "PATCH", + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "data": [], + "dataMode": "raw", + "name": "PATCH http://localhost:5000/posts/1", + "description": "", + "descriptionFormat": "html", + "time": 1477768379775, + "version": 2, + "responses": [], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "rawModeData": "{\n \"title\": \"gfdgsgsdgsdfgsdfgdfg\",\n}" + }, + { + "id": "e8825dc3-4137-99a7-0000-ef5786610dc3", + "headers": "", + "url": "http://localhost:5000/posts", + "pathVariables": {}, + "preRequestScript": null, + "method": "POST", + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "data": [], + "dataMode": "raw", + "name": "POST http://localhost:5000/posts/1", + "description": "", + "descriptionFormat": "html", + "time": 1477768186023, + "version": 2, + "responses": [], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "rawModeData": "{\n \"userId\": 1,\n \"title\": \"test\",\n \"body\": \"test\"\n}" + }, + { + "id": "ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2", + "headers": "", + "url": "http://localhost:5000/posts/1", + "pathVariables": {}, + "preRequestScript": null, + "method": "GET", + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "data": null, + "dataMode": "params", + "name": "GET http://localhost:5000/posts/1", + "description": "", + "descriptionFormat": "html", + "time": 1477768023989, + "version": 2, + "responses": [], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {} + }, + { + "id": "fddfc4fa-5114-69e3-4744-203ed71a526b", + "headers": "", + "url": "http://localhost:5000/posts/1", + "pathVariables": {}, + "preRequestScript": null, + "method": "PUT", + "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", + "data": [], + "dataMode": "raw", + "name": "PUT http://localhost:5000/posts/1", + "description": "", + "descriptionFormat": "html", + "time": 1477768307036, + "version": 2, + "responses": [], + "tests": null, + "currentHelper": "normal", + "helperAttributes": {}, + "rawModeData": "{\n \"userId\": 1,\n \"title\": \"test\",\n \"body\": \"test\"\n}" + } + ] +} \ No newline at end of file diff --git a/src/Ocelot.ApiGateway/Program.cs b/test/Ocelot.ManualTest/Program.cs similarity index 76% rename from src/Ocelot.ApiGateway/Program.cs rename to test/Ocelot.ManualTest/Program.cs index b5404270..a049d3ea 100644 --- a/src/Ocelot.ApiGateway/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -1,8 +1,7 @@ -using System.IO; +using System.IO; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Builder; -namespace Ocelot.ApiGateway +namespace Ocelot.ManualTest { public class Program { @@ -11,7 +10,6 @@ namespace Ocelot.ApiGateway var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() .UseStartup() .Build(); diff --git a/src/Ocelot.ApiGateway/Properties/launchSettings.json b/test/Ocelot.ManualTest/Properties/launchSettings.json similarity index 60% rename from src/Ocelot.ApiGateway/Properties/launchSettings.json rename to test/Ocelot.ManualTest/Properties/launchSettings.json index 3f72d44b..bfc47fdf 100644 --- a/src/Ocelot.ApiGateway/Properties/launchSettings.json +++ b/test/Ocelot.ManualTest/Properties/launchSettings.json @@ -3,26 +3,24 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:55368/", + "applicationUrl": "http://localhost:24620/", "sslPort": 0 } }, "profiles": { "IIS Express": { - "commandName" : "IISExpress", + "commandName": "IISExpress", "launchBrowser": true, - "launchUrl": "api/values", - "environmentVariables" : { + "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, - "Ocelot.ApiGateway": { + "Ocelot.ManualTest": { "commandName": "Project", - "launchBrowser": true, - "launchUrl": "http://localhost:5000/api/values", + "launchUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } -} +} \ No newline at end of file diff --git a/src/Ocelot.ApiGateway/Startup.cs b/test/Ocelot.ManualTest/Startup.cs similarity index 58% rename from src/Ocelot.ApiGateway/Startup.cs rename to test/Ocelot.ManualTest/Startup.cs index c3c49020..70448fcf 100644 --- a/src/Ocelot.ApiGateway/Startup.cs +++ b/test/Ocelot.ManualTest/Startup.cs @@ -1,11 +1,15 @@ +using System; +using CacheManager.Core; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Routing; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; -namespace Ocelot.ApiGateway +namespace Ocelot.ManualTest { public class Startup { @@ -15,30 +19,35 @@ namespace Ocelot.ApiGateway .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; } - // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddRouting(); + Action settings = (x) => + { + x.WithMicrosoftLogging(log => + { + log.AddConsole(LogLevel.Debug); + }) + .WithDictionaryHandle(); + }; + + services.AddOcelotOutputCaching(settings); + services.AddOcelotFileConfiguration(Configuration); + services.AddOcelot(); } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - - var routeBuilder = new RouteBuilder(app); - - routeBuilder.AddRouter(app); - - app.UseRouter(routeBuilder.Build()); + app.UseOcelot(); } } } diff --git a/src/Ocelot.ApiGateway/appsettings.json b/test/Ocelot.ManualTest/appsettings.json old mode 100755 new mode 100644 similarity index 77% rename from src/Ocelot.ApiGateway/appsettings.json rename to test/Ocelot.ManualTest/appsettings.json index cb1818e7..d73b7dcb --- a/src/Ocelot.ApiGateway/appsettings.json +++ b/test/Ocelot.ManualTest/appsettings.json @@ -1,10 +1,10 @@ -{ - "Logging": { - "IncludeScopes": false, - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} +{ + "Logging": { + "IncludeScopes": true, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/test/Ocelot.ManualTest/configuration.json b/test/Ocelot.ManualTest/configuration.json new file mode 100644 index 00000000..f7e2bb75 --- /dev/null +++ b/test/Ocelot.ManualTest/configuration.json @@ -0,0 +1,211 @@ +{ + "ReRoutes": [ + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHost": "localhost", + "DownstreamPort": 52876, + "UpstreamTemplate": "/identityserverexample", + "UpstreamHttpMethod": "Get", + "AuthenticationOptions": { + "Provider": "IdentityServer", + "ProviderRootUrl": "http://localhost:52888", + "ScopeName": "api", + "AdditionalScopes": [ + "openid", + "offline_access" + ], + "ScopeSecret": "secret" + }, + "AddHeadersToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "AddClaimsToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "AddQueriesToRequest": { + "CustomerId": "Claims[CustomerId] > value", + "LocationId": "Claims[LocationId] > value", + "UserType": "Claims[sub] > value[0] > |", + "UserId": "Claims[sub] > value[1] > |" + }, + "RouteClaimsRequirement": { + "UserType": "registered" + }, + "RequestIdKey": "OcRequestId" + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/posts", + "UpstreamHttpMethod": "Get", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Get" + }, + { + "DownstreamPathTemplate": "/posts/{postId}/comments", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/posts/{postId}/comments", + "UpstreamHttpMethod": "Get" + }, + { + "DownstreamPathTemplate": "/comments", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/comments", + "UpstreamHttpMethod": "Get" + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/posts", + "UpstreamHttpMethod": "Post" + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Put" + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Patch" + }, + { + "DownstreamPathTemplate": "/posts/{postId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/posts/{postId}", + "UpstreamHttpMethod": "Delete" + }, + { + "DownstreamPathTemplate": "/api/products", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/products", + "UpstreamHttpMethod": "Get", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/products/{productId}", + "UpstreamHttpMethod": "Get", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products", + "DownstreamScheme": "http", + "DownstreamHost": "products20161126090340.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamTemplate": "/products", + "UpstreamHttpMethod": "Post", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHost": "products20161126090340.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamTemplate": "/products/{productId}", + "UpstreamHttpMethod": "Put", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/products/{productId}", + "DownstreamScheme": "http", + "DownstreamHost": "products20161126090340.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamTemplate": "/products/{productId}", + "UpstreamHttpMethod": "Delete", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamTemplate": "/customers", + "UpstreamHttpMethod": "Get", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers/{customerId}", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamTemplate": "/customers/{customerId}", + "UpstreamHttpMethod": "Get", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamTemplate": "/customers", + "UpstreamHttpMethod": "Post", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers/{customerId}", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamTemplate": "/customers/{customerId}", + "UpstreamHttpMethod": "Put", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/api/customers/{customerId}", + "DownstreamScheme": "http", + "DownstreamHost": "customers20161126090811.azurewebsites.net", + "DownstreamPort": 80, + "UpstreamTemplate": "/customers/{customerId}", + "UpstreamHttpMethod": "Delete", + "FileCacheOptions": { "TtlSeconds": 15 } + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "http", + "DownstreamHost": "jsonplaceholder.typicode.com", + "DownstreamPort": 80, + "UpstreamTemplate": "/posts/", + "UpstreamHttpMethod": "Get", + "FileCacheOptions": { "TtlSeconds": 15 } + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId" + } +} \ No newline at end of file diff --git a/test/Ocelot.ManualTest/project.json b/test/Ocelot.ManualTest/project.json new file mode 100644 index 00000000..3ae09ccb --- /dev/null +++ b/test/Ocelot.ManualTest/project.json @@ -0,0 +1,63 @@ +{ + "version": "0.0.0-dev", + + "dependencies": { + "Microsoft.AspNetCore.Http": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Ocelot": "0.0.0-dev", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0" + }, + + "tools": { + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" + }, + "runtimes": { + "win10-x64": {}, + "osx.10.11-x64":{}, + "win7-x64": {} + }, + "frameworks": { + "netcoreapp1.4": { + "imports": [ + ] + } + }, + + "buildOptions": { + "emitEntryPoint": true, + "preserveCompilationContext": true, + "copyToOutput": { + "include": [ + "configuration.json" + ] + } + }, + + "runtimeOptions": { + "configProperties": { + "System.GC.Server": true + } + }, + + "publishOptions": { + "include": [ + "wwwroot", + "Views", + "Areas/**/Views", + "appsettings.json", + "web.config", + "configuration.json" + ] + }, + + "scripts": { + "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ] + } +} diff --git a/src/Ocelot.ApiGateway/web.config b/test/Ocelot.ManualTest/web.config similarity index 80% rename from src/Ocelot.ApiGateway/web.config rename to test/Ocelot.ManualTest/web.config index 68ab630e..dc0514fc 100644 --- a/src/Ocelot.ApiGateway/web.config +++ b/test/Ocelot.ManualTest/web.config @@ -1,14 +1,14 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/test/Ocelot.UnitTests/.gitignore b/test/Ocelot.UnitTests/.gitignore deleted file mode 100644 index 0ca27f04..00000000 --- a/test/Ocelot.UnitTests/.gitignore +++ /dev/null @@ -1,234 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Microsoft Azure ApplicationInsights config file -ApplicationInsights.config - -# Windows Store app package directory -AppPackages/ -BundleArtifacts/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe - -# FAKE - F# Make -.fake/ diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationHandlerFactoryTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationHandlerFactoryTests.cs new file mode 100644 index 00000000..e76a2b28 --- /dev/null +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationHandlerFactoryTests.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Authentication.Handler; +using Ocelot.Authentication.Handler.Creator; +using Ocelot.Authentication.Handler.Factory; +using Ocelot.Errors; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; +using AuthenticationOptions = Ocelot.Configuration.AuthenticationOptions; + +namespace Ocelot.UnitTests.Authentication +{ + public class AuthenticationHandlerFactoryTests + { + private readonly IAuthenticationHandlerFactory _authenticationHandlerFactory; + private readonly Mock _app; + private readonly Mock _creator; + private AuthenticationOptions _authenticationOptions; + private Response _result; + + public AuthenticationHandlerFactoryTests() + { + _app = new Mock(); + _creator = new Mock(); + _authenticationHandlerFactory = new AuthenticationHandlerFactory(_creator.Object); + } + + [Fact] + public void should_return_identity_server_access_token_handler() + { + this.Given(x => x.GivenTheAuthenticationOptionsAre(new AuthenticationOptions("IdentityServer", "","",false, new List(), ""))) + .And(x => x.GivenTheCreatorReturns()) + .When(x => x.WhenIGetFromTheFactory()) + .Then(x => x.ThenTheHandlerIsReturned("IdentityServer")) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_cannot_create_handler() + { + this.Given(x => x.GivenTheAuthenticationOptionsAre(new AuthenticationOptions("IdentityServer", "", "", false, new List(), ""))) + .And(x => x.GivenTheCreatorReturnsAnError()) + .When(x => x.WhenIGetFromTheFactory()) + .Then(x => x.ThenAnErrorResponseIsReturned()) + .BDDfy(); + } + + private void GivenTheAuthenticationOptionsAre(AuthenticationOptions authenticationOptions) + { + _authenticationOptions = authenticationOptions; + } + + private void GivenTheCreatorReturnsAnError() + { + _creator + .Setup(x => x.CreateIdentityServerAuthenticationHandler(It.IsAny(), It.IsAny())) + .Returns(new ErrorResponse(new List + { + new UnableToCreateAuthenticationHandlerError($"Unable to create authentication handler for xxx") + })); + } + + private void GivenTheCreatorReturns() + { + _creator + .Setup(x => x.CreateIdentityServerAuthenticationHandler(It.IsAny(), It.IsAny())) + .Returns(new OkResponse(x => Task.CompletedTask)); + } + + private void WhenIGetFromTheFactory() + { + _result = _authenticationHandlerFactory.Get(_app.Object, _authenticationOptions); + } + + private void ThenTheHandlerIsReturned(string expected) + { + _result.Data.Provider.ShouldBe(expected); + } + + private void ThenAnErrorResponseIsReturned() + { + _result.IsError.ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs new file mode 100644 index 00000000..ff467aa7 --- /dev/null +++ b/test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Authentication.Handler; +using Ocelot.Authentication.Handler.Factory; +using Ocelot.Authentication.Middleware; +using Ocelot.Cache.Middleware; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Authentication +{ + public class AuthenticationMiddlewareTests : IDisposable + { + private readonly Mock _scopedRepository; + private readonly Mock _authFactory; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private HttpResponseMessage _result; + private OkResponse _downstreamRoute; + + public AuthenticationMiddlewareTests() + { + _url = "http://localhost:51879"; + _scopedRepository = new Mock(); + _authFactory = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_authFactory.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseAuthenticationMiddleware(); + + app.Run(async x => + { + await x.Response.WriteAsync("The user is authenticated"); + }); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_next_middleware_if_route_is_not_authenticated() + { + this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), new ReRouteBuilder().Build()))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheUserIsAuthenticated()) + .BDDfy(); + } + + private void ThenTheUserIsAuthenticated() + { + var content = _result.Content.ReadAsStringAsync().Result; + content.ShouldBe("The user is authenticated"); + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs b/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs new file mode 100644 index 00000000..8d681e03 --- /dev/null +++ b/test/Ocelot.UnitTests/Authorization/AuthorisationMiddlewareTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Security.Claims; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Authorisation; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Responses; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Authorization +{ + using Authorisation.Middleware; + + public class AuthorisationMiddlewareTests : IDisposable + { + private readonly Mock _scopedRepository; + private readonly Mock _authService; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private HttpResponseMessage _result; + private OkResponse _downstreamRoute; + + public AuthorisationMiddlewareTests() + { + _url = "http://localhost:51879"; + _scopedRepository = new Mock(); + _authService = new Mock(); + + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_authService.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseAuthorisationMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_authorisation_service() + { + this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), new ReRouteBuilder().WithIsAuthorised(true).Build()))) + .And(x => x.GivenTheAuthServiceReturns(new OkResponse(true))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheAuthServiceIsCalledCorrectly()) + .BDDfy(); + } + + private void GivenTheAuthServiceReturns(Response expected) + { + _authService + .Setup(x => x.Authorise(It.IsAny(), It.IsAny>())) + .Returns(expected); + } + + private void ThenTheAuthServiceIsCalledCorrectly() + { + _authService + .Verify(x => x.Authorise(It.IsAny(), + It.IsAny>()), Times.Once); + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Authorization/ClaimsAuthoriserTests.cs b/test/Ocelot.UnitTests/Authorization/ClaimsAuthoriserTests.cs new file mode 100644 index 00000000..7b3779a6 --- /dev/null +++ b/test/Ocelot.UnitTests/Authorization/ClaimsAuthoriserTests.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Security.Claims; +using Ocelot.Authorisation; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Authorization +{ + using Ocelot.Infrastructure.Claims.Parser; + + public class ClaimsAuthoriserTests + { + private readonly ClaimsAuthoriser _claimsAuthoriser; + private ClaimsPrincipal _claimsPrincipal; + private Dictionary _requirement; + private Response _result; + + public ClaimsAuthoriserTests() + { + _claimsAuthoriser = new ClaimsAuthoriser(new ClaimsParser()); + } + + [Fact] + public void should_authorise_user() + { + this.Given(x => x.GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim("UserType", "registered") + })))) + .And(x => x.GivenARouteClaimsRequirement(new Dictionary + { + {"UserType", "registered"} + })) + .When(x => x.WhenICallTheAuthoriser()) + .Then(x => x.ThenTheUserIsAuthorised()) + .BDDfy(); + } + + [Fact] + public void should_not_authorise_user() + { + this.Given(x => x.GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List())))) + .And(x => x.GivenARouteClaimsRequirement(new Dictionary + { + { "UserType", "registered" } + })) + .When(x => x.WhenICallTheAuthoriser()) + .Then(x => x.ThenTheUserIsntAuthorised()) + .BDDfy(); + } + + private void GivenAClaimsPrincipal(ClaimsPrincipal claimsPrincipal) + { + _claimsPrincipal = claimsPrincipal; + } + + private void GivenARouteClaimsRequirement(Dictionary requirement) + { + _requirement = requirement; + } + + private void WhenICallTheAuthoriser() + { + _result = _claimsAuthoriser.Authorise(_claimsPrincipal, _requirement); + } + + private void ThenTheUserIsAuthorised() + { + _result.Data.ShouldBe(true); + } + + private void ThenTheUserIsntAuthorised() + { + _result.Data.ShouldBe(false); + } + } +} diff --git a/test/Ocelot.UnitTests/Cache/CacheManagerCacheTests.cs b/test/Ocelot.UnitTests/Cache/CacheManagerCacheTests.cs new file mode 100644 index 00000000..87752a52 --- /dev/null +++ b/test/Ocelot.UnitTests/Cache/CacheManagerCacheTests.cs @@ -0,0 +1,77 @@ +using System; +using CacheManager.Core; +using Moq; +using Ocelot.Cache; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Cache +{ + public class CacheManagerCacheTests + { + private OcelotCacheManagerCache _ocelotOcelotCacheManager; + private Mock> _mockCacheManager; + private string _key; + private string _value; + private string _resultGet; + private TimeSpan _ttlSeconds; + + public CacheManagerCacheTests() + { + _mockCacheManager = new Mock>(); + _ocelotOcelotCacheManager = new OcelotCacheManagerCache(_mockCacheManager.Object); + } + [Fact] + public void should_get_from_cache() + { + this.Given(x => x.GivenTheFollowingIsCached("someKey", "someValue")) + .When(x => x.WhenIGetFromTheCache()) + .Then(x => x.ThenTheResultIs("someValue")) + .BDDfy(); + + } + + [Fact] + public void should_add_to_cache() + { + this.When(x => x.WhenIAddToTheCache("someKey", "someValue", TimeSpan.FromSeconds(1))) + .Then(x => x.ThenTheCacheIsCalledCorrectly()) + .BDDfy(); + } + + private void WhenIAddToTheCache(string key, string value, TimeSpan ttlSeconds) + { + _key = key; + _value = value; + _ttlSeconds = ttlSeconds; + + _ocelotOcelotCacheManager.Add(_key, _value, _ttlSeconds); + } + + private void ThenTheCacheIsCalledCorrectly() + { + _mockCacheManager + .Verify(x => x.Add(It.IsAny>()), Times.Once); + } + + private void ThenTheResultIs(string expected) + { + _resultGet.ShouldBe(expected); + } + + private void WhenIGetFromTheCache() + { + _resultGet = _ocelotOcelotCacheManager.Get(_key); + } + + private void GivenTheFollowingIsCached(string key, string value) + { + _key = key; + _value = value; + _mockCacheManager + .Setup(x => x.Get(It.IsAny())) + .Returns(value); + } + } +} diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs new file mode 100644 index 00000000..9190dbe9 --- /dev/null +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using CacheManager.Core; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Cache; +using Ocelot.Cache.Middleware; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Responses; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Cache +{ + public class OutputCacheMiddlewareTests + { + private readonly Mock> _cacheManager; + private readonly Mock _scopedRepo; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private HttpResponseMessage _result; + private HttpResponseMessage _response; + + public OutputCacheMiddlewareTests() + { + _cacheManager = new Mock>(); + _scopedRepo = new Mock(); + + + _url = "http://localhost:51879"; + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_cacheManager.Object); + x.AddSingleton(_scopedRepo.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseOutputCacheMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_returned_cached_item_when_it_is_in_cache() + { + this.Given(x => x.GivenThereIsACachedResponse(new HttpResponseMessage())) + .And(x => x.GivenTheDownstreamRouteIs()) + .And(x => x.GivenThereIsADownstreamUrl()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheCacheGetIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_continue_with_pipeline_and_cache_response() + { + this.Given(x => x.GivenResponseIsNotCached()) + .And(x => x.GivenTheDownstreamRouteIs()) + .And(x => x.GivenThereAreNoErrors()) + .And(x => x.GivenThereIsADownstreamUrl()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheCacheAddIsCalledCorrectly()) + .BDDfy(); + } + + + private void GivenTheDownstreamRouteIs() + { + var reRoute = new ReRouteBuilder().WithIsCached(true).WithCacheOptions(new CacheOptions(100)).Build(); + var downstreamRoute = new DownstreamRoute(new List(), reRoute); + + _scopedRepo + .Setup(x => x.Get(It.IsAny())) + .Returns(new OkResponse(downstreamRoute)); + } + + private void GivenThereAreNoErrors() + { + _scopedRepo + .Setup(x => x.Get("OcelotMiddlewareError")) + .Returns(new OkResponse(false)); + } + + private void GivenThereIsADownstreamUrl() + { + _scopedRepo + .Setup(x => x.Get("DownstreamUrl")) + .Returns(new OkResponse("anything")); + } + + private void ThenTheCacheGetIsCalledCorrectly() + { + _cacheManager + .Verify(x => x.Get(It.IsAny()), Times.Once); + } + + private void ThenTheCacheAddIsCalledCorrectly() + { + _cacheManager + .Verify(x => x.Add(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + private void GivenResponseIsNotCached() + { + _scopedRepo + .Setup(x => x.Get("HttpResponseMessage")) + .Returns(new OkResponse(new HttpResponseMessage())); + } + + private void GivenThereIsACachedResponse(HttpResponseMessage response) + { + _response = response; + _cacheManager + .Setup(x => x.Get(It.IsAny())) + .Returns(_response); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + } +} diff --git a/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs b/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs new file mode 100644 index 00000000..e88fc773 --- /dev/null +++ b/test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Claims; +using Ocelot.Configuration; +using Ocelot.Errors; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Claims +{ + public class AddClaimsToRequestTests + { + private readonly AddClaimsToRequest _addClaimsToRequest; + private readonly Mock _parser; + private List _claimsToThings; + private HttpContext _context; + private Response _result; + private Response _claimValue; + + public AddClaimsToRequestTests() + { + _parser = new Mock(); + _addClaimsToRequest = new AddClaimsToRequest(_parser.Object); + } + + [Fact] + public void should_add_claims_to_context() + { + var context = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim("test", "data") + })) + }; + + this.Given( + x => x.GivenClaimsToThings(new List + { + new ClaimToThing("claim-key", "", "", 0) + })) + .Given(x => x.GivenHttpContext(context)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddClaimsToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .BDDfy(); + } + + [Fact] + public void if_claims_exists_should_replace_it() + { + var context = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim("existing-key", "data"), + new Claim("new-key", "data") + })), + }; + + this.Given( + x => x.GivenClaimsToThings(new List + { + new ClaimToThing("existing-key", "new-key", "", 0) + })) + .Given(x => x.GivenHttpContext(context)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddClaimsToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + this.Given( + x => x.GivenClaimsToThings(new List + { + new ClaimToThing("", "", "", 0) + })) + .Given(x => x.GivenHttpContext(new DefaultHttpContext())) + .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List + { + new AnyError() + }))) + .When(x => x.WhenIAddClaimsToTheRequest()) + .Then(x => x.ThenTheResultIsError()) + .BDDfy(); + } + + + private void GivenClaimsToThings(List configuration) + { + _claimsToThings = configuration; + } + + private void GivenHttpContext(HttpContext context) + { + _context = context; + } + + private void GivenTheClaimParserReturns(Response claimValue) + { + _claimValue = claimValue; + _parser + .Setup( + x => + x.GetValue(It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(_claimValue); + } + + private void WhenIAddClaimsToTheRequest() + { + _result = _addClaimsToRequest.SetClaimsOnContext(_claimsToThings, _context); + } + + private void ThenTheResultIsSuccess() + { + _result.IsError.ShouldBe(false); + } + + private void ThenTheResultIsError() + { + + _result.IsError.ShouldBe(true); + } + + class AnyError : Error + { + public AnyError() + : base("blahh", OcelotErrorCode.UnknownError) + { + } + } + } +} diff --git a/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs new file mode 100644 index 00000000..8822e6b2 --- /dev/null +++ b/test/Ocelot.UnitTests/Claims/ClaimsBuilderMiddlewareTests.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Cache.Middleware; +using Ocelot.Claims; +using Ocelot.Claims.Middleware; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Responses; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Claims +{ + public class ClaimsBuilderMiddlewareTests : IDisposable + { + private readonly Mock _scopedRepository; + private readonly Mock _addHeaders; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private Response _downstreamRoute; + private HttpResponseMessage _result; + + public ClaimsBuilderMiddlewareTests() + { + _url = "http://localhost:51879"; + _scopedRepository = new Mock(); + _addHeaders = new Mock(); + + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + + x.AddLogging(); + x.AddSingleton(_addHeaders.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseClaimsBuilderMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_claims_to_request_correctly() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithClaimsToClaims(new List + { + new ClaimToThing("sub", "UserType", "|", 0) + }) + .Build()); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheAddClaimsToRequestReturns()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheClaimsToRequestIsCalledCorrectly()) + .BDDfy(); + } + + private void GivenTheAddClaimsToRequestReturns() + { + _addHeaders + .Setup(x => x.SetClaimsOnContext(It.IsAny>(), + It.IsAny())) + .Returns(new OkResponse()); + } + + private void ThenTheClaimsToRequestIsCalledCorrectly() + { + _addHeaders + .Verify(x => x.SetClaimsOnContext(It.IsAny>(), + It.IsAny()), Times.Once); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs new file mode 100644 index 00000000..1f3ba9bc --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using System.Linq; +using Ocelot.Configuration; +using Ocelot.Configuration.Parser; +using Ocelot.Errors; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class ClaimToThingConfigurationParserTests + { + private Dictionary _dictionary; + private readonly IClaimToThingConfigurationParser _claimToThingConfigurationParser; + private Response _result; + + public ClaimToThingConfigurationParserTests() + { + _claimToThingConfigurationParser = new ClaimToThingConfigurationParser(); + } + + [Fact] + public void returns_no_instructions_error() + { + this.Given(x => x.GivenTheDictionaryIs(new Dictionary() + { + {"CustomerId", ""}, + })) + .When(x => x.WhenICallTheExtractor()) + .Then( + x => + x.ThenAnErrorIsReturned(new ErrorResponse( + new List + { + new NoInstructionsError(">") + }))) + .BDDfy(); + } + + [Fact] + public void returns_no_instructions_not_for_claims_error() + { + this.Given(x => x.GivenTheDictionaryIs(new Dictionary() + { + {"CustomerId", "Cheese[CustomerId] > value"}, + })) + .When(x => x.WhenICallTheExtractor()) + .Then( + x => + x.ThenAnErrorIsReturned(new ErrorResponse( + new List + { + new InstructionNotForClaimsError() + }))) + .BDDfy(); + } + + [Fact] + public void can_parse_entry_to_work_out_properties_with_key() + { + this.Given(x => x.GivenTheDictionaryIs(new Dictionary() + { + {"CustomerId", "Claims[CustomerId] > value"}, + })) + .When(x => x.WhenICallTheExtractor()) + .Then( + x => + x.ThenTheClaimParserPropertiesAreReturned( + new OkResponse( + new ClaimToThing("CustomerId", "CustomerId", "", 0)))) + .BDDfy(); + } + + [Fact] + public void can_parse_entry_to_work_out_properties_with_key_delimiter_and_index() + { + this.Given(x => x.GivenTheDictionaryIs(new Dictionary() + { + {"UserId", "Claims[Subject] > value[0] > |"}, + })) + .When(x => x.WhenICallTheExtractor()) + .Then( + x => + x.ThenTheClaimParserPropertiesAreReturned( + new OkResponse( + new ClaimToThing("UserId", "Subject", "|", 0)))) + .BDDfy(); + } + + private void ThenAnErrorIsReturned(Response expected) + { + _result.IsError.ShouldBe(expected.IsError); + _result.Errors[0].ShouldBeOfType(expected.Errors[0].GetType()); + } + + private void ThenTheClaimParserPropertiesAreReturned(Response expected) + { + _result.Data.NewKey.ShouldBe(expected.Data.NewKey); + _result.Data.Delimiter.ShouldBe(expected.Data.Delimiter); + _result.Data.Index.ShouldBe(expected.Data.Index); + _result.IsError.ShouldBe(expected.IsError); + } + + private void WhenICallTheExtractor() + { + var first = _dictionary.First(); + _result = _claimToThingConfigurationParser.Extract(first.Key, first.Value); + } + + private void GivenTheDictionaryIs(Dictionary dictionary) + { + _dictionary = dictionary; + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs b/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs new file mode 100644 index 00000000..8a3e24f9 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/ConfigurationValidationTests.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Validator; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class ConfigurationValidationTests + { + private readonly IConfigurationValidator _configurationValidator; + private FileConfiguration _fileConfiguration; + private Response _result; + + public ConfigurationValidationTests() + { + _configurationValidator = new FileConfigurationValidator(); + } + + [Fact] + public void configuration_is_invalid_if_scheme_in_downstream_template() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "http://www.bbc.co.uk/api/products/{productId}", + UpstreamTemplate = "http://asdf.com" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_one_reroute() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamTemplate = "http://asdf.com" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_valid_with_valid_authentication_provider() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamTemplate = "http://asdf.com", + AuthenticationOptions = new FileAuthenticationOptions + { + Provider = "IdentityServer" + } + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsValid()) + .BDDfy(); + } + + [Fact] + public void configuration_is_invalid_with_invalid_authentication_provider() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamTemplate = "http://asdf.com", + AuthenticationOptions = new FileAuthenticationOptions + { + Provider = "BootyBootyBottyRockinEverywhere" + } + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorIs()) + .BDDfy(); + } + + [Fact] + public void configuration_is_not_valid_with_duplicate_reroutes() + { + this.Given(x => x.GivenAConfiguration(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/products/", + UpstreamTemplate = "http://asdf.com" + }, + new FileReRoute + { + DownstreamPathTemplate = "http://www.bbc.co.uk", + UpstreamTemplate = "http://asdf.com" + } + } + })) + .When(x => x.WhenIValidateTheConfiguration()) + .Then(x => x.ThenTheResultIsNotValid()) + .And(x => x.ThenTheErrorIs()) + .BDDfy(); + } + + private void GivenAConfiguration(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + } + + private void WhenIValidateTheConfiguration() + { + _result = _configurationValidator.IsValid(_fileConfiguration); + } + + private void ThenTheResultIsValid() + { + _result.Data.IsError.ShouldBeFalse(); + } + + private void ThenTheResultIsNotValid() + { + _result.Data.IsError.ShouldBeTrue(); + } + + private void ThenTheErrorIs() + { + _result.Data.Errors[0].ShouldBeOfType(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs new file mode 100644 index 00000000..fc80a478 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -0,0 +1,580 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Parser; +using Ocelot.Configuration.Validator; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class FileConfigurationCreatorTests + { + private readonly Mock> _fileConfig; + private readonly Mock _validator; + private Response _config; + private FileConfiguration _fileConfiguration; + private readonly Mock _configParser; + private readonly Mock> _logger; + private readonly FileOcelotConfigurationCreator _ocelotConfigurationCreator; + + public FileConfigurationCreatorTests() + { + _logger = new Mock>(); + _configParser = new Mock(); + _validator = new Mock(); + _fileConfig = new Mock>(); + _ocelotConfigurationCreator = new FileOcelotConfigurationCreator( + _fileConfig.Object, _validator.Object, _configParser.Object, _logger.Object); + } + + [Fact] + public void should_use_downstream_host() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHost = "127.0.0.1", + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamHost("127.0.0.1") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .Build() + })) + .BDDfy(); + } + + public void should_use_downstream_scheme() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamScheme = "https", + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + } + }, + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamScheme("https") + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_use_service_discovery_for_downstream_service_host() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = false, + ServiceName = "ProductService" + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Provider = "consul", + Address = "127.0.0.1" + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .WithServiceName("ProductService") + .WithUseServiceDiscovery(true) + .WithServiceDiscoveryProvider("consul") + .WithServiceDiscoveryAddress("127.0.01") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_not_use_service_discovery_for_downstream_host_url_when_no_service_name() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = false, + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .WithUseServiceDiscovery(false) + .WithServiceDiscoveryProvider(null) + .WithServiceDiscoveryAddress(null) + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_use_reroute_case_sensitivity_value() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = false + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_set_upstream_template_pattern_to_ignore_case_sensitivity() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get" + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("(?i)/api/products/.*/$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_set_upstream_template_pattern_to_respect_case_sensitivity() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("/api/products/.*/$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_set_global_request_id_key() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + RequestIdKey = "blahhhh" + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("/api/products/.*/$") + .WithRequestIdKey("blahhhh") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_anything_to_end_of_string() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("/api/products/.*/$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_create_with_headers_to_extract() + { + var expected = new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("/api/products/.*/$") + .WithAuthenticationProvider("IdentityServer") + .WithAuthenticationProviderUrl("http://localhost:51888") + .WithRequireHttps(false) + .WithScopeSecret("secret") + .WithAuthenticationProviderScopeName("api") + .WithClaimsToHeaders(new List + { + new ClaimToThing("CustomerId", "CustomerId", "", 0), + }) + .Build() + }; + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true, + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List(), + Provider = "IdentityServer", + ProviderRootUrl = "http://localhost:51888", + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret" + }, + AddHeadersToRequest = + { + {"CustomerId", "Claims[CustomerId] > value"}, + } + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .And(x => x.GivenTheConfigHeaderExtractorReturns(new ClaimToThing("CustomerId", "CustomerId", "", 0))) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(expected)) + .And(x => x.ThenTheAuthenticationOptionsAre(expected)) + .BDDfy(); + } + + private void GivenTheConfigHeaderExtractorReturns(ClaimToThing expected) + { + _configParser + .Setup(x => x.Extract(It.IsAny(), It.IsAny())) + .Returns(new OkResponse(expected)); + } + + [Fact] + public void should_create_with_authentication_properties() + { + var expected = new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("/api/products/.*/$") + .WithAuthenticationProvider("IdentityServer") + .WithAuthenticationProviderUrl("http://localhost:51888") + .WithRequireHttps(false) + .WithScopeSecret("secret") + .WithAuthenticationProviderScopeName("api") + .Build() + }; + + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true, + AuthenticationOptions = new FileAuthenticationOptions + { + AdditionalScopes = new List(), + Provider = "IdentityServer", + ProviderRootUrl = "http://localhost:51888", + RequireHttps = false, + ScopeName = "api", + ScopeSecret = "secret" + } + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(expected)) + .And(x => x.ThenTheAuthenticationOptionsAre(expected)) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_more_than_one_placeholder() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}/variants/{variantId}", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}/variants/{variantId}") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("/api/products/.*/variants/.*/$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_more_than_one_placeholder_with_trailing_slash() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/api/products/{productId}/variants/{variantId}/", + DownstreamPathTemplate = "/products/{productId}", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/products/{productId}") + .WithUpstreamTemplate("/api/products/{productId}/variants/{variantId}/") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("/api/products/.*/variants/.*/$") + .Build() + })) + .BDDfy(); + } + + [Fact] + public void should_create_template_pattern_that_matches_to_end_of_string() + { + this.Given(x => x.GivenTheConfigIs(new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamTemplate = "/", + DownstreamPathTemplate = "/api/products/", + UpstreamHttpMethod = "Get", + ReRouteIsCaseSensitive = true + } + } + })) + .And(x => x.GivenTheConfigIsValid()) + .When(x => x.WhenICreateTheConfig()) + .Then(x => x.ThenTheReRoutesAre(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("/api/products/") + .WithUpstreamTemplate("/") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("/$") + .Build() + })) + .BDDfy(); + } + + private void GivenTheConfigIsValid() + { + _validator + .Setup(x => x.IsValid(It.IsAny())) + .Returns(new OkResponse(new ConfigurationValidationResult(false))); + } + + private void GivenTheConfigIs(FileConfiguration fileConfiguration) + { + _fileConfiguration = fileConfiguration; + _fileConfig + .Setup(x => x.Value) + .Returns(_fileConfiguration); + } + + private void WhenICreateTheConfig() + { + _config = _ocelotConfigurationCreator.Create(); + } + + private void ThenTheReRoutesAre(List expectedReRoutes) + { + for (int i = 0; i < _config.Data.ReRoutes.Count; i++) + { + var result = _config.Data.ReRoutes[i]; + var expected = expectedReRoutes[i]; + + result.DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate.Value); + result.UpstreamHttpMethod.ShouldBe(expected.UpstreamHttpMethod); + result.UpstreamTemplate.ShouldBe(expected.UpstreamTemplate); + result.UpstreamTemplatePattern.ShouldBe(expected.UpstreamTemplatePattern); + } + } + + private void ThenTheAuthenticationOptionsAre(List expectedReRoutes) + { + for (int i = 0; i < _config.Data.ReRoutes.Count; i++) + { + var result = _config.Data.ReRoutes[i].AuthenticationOptions; + var expected = expectedReRoutes[i].AuthenticationOptions; + + result.AdditionalScopes.ShouldBe(expected.AdditionalScopes); + result.Provider.ShouldBe(expected.Provider); + result.ProviderRootUrl.ShouldBe(expected.ProviderRootUrl); + result.RequireHttps.ShouldBe(expected.RequireHttps); + result.ScopeName.ShouldBe(expected.ScopeName); + result.ScopeSecret.ShouldBe(expected.ScopeSecret); + + } + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs new file mode 100644 index 00000000..56fb6487 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs @@ -0,0 +1,112 @@ +using System.Collections.Generic; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.Provider; +using Ocelot.Configuration.Repository; +using Ocelot.Errors; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class FileConfigurationProviderTests + { + private readonly IOcelotConfigurationProvider _ocelotConfigurationProvider; + private readonly Mock _configurationRepository; + private readonly Mock _creator; + private Response _result; + + public FileConfigurationProviderTests() + { + _creator = new Mock(); + _configurationRepository = new Mock(); + _ocelotConfigurationProvider = new OcelotConfigurationProvider(_configurationRepository.Object, _creator.Object); + } + + [Fact] + public void should_get_config() + { + this.Given(x => x.GivenTheRepoReturns(new OkResponse(new OcelotConfiguration(new List())))) + .When(x => x.WhenIGetTheConfig()) + .Then(x => x.TheFollowingIsReturned(new OkResponse(new OcelotConfiguration(new List())))) + .BDDfy(); + } + + [Fact] + public void should_create_config_if_it_doesnt_exist() + { + this.Given(x => x.GivenTheRepoReturns(new OkResponse(null))) + .And(x => x.GivenTheCreatorReturns(new OkResponse(new OcelotConfiguration(new List())))) + .When(x => x.WhenIGetTheConfig()) + .Then(x => x.TheFollowingIsReturned(new OkResponse(new OcelotConfiguration(new List())))) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + this.Given(x => x.GivenTheRepoReturns(new ErrorResponse(new List + { + new AnyError() + }))) + .When(x => x.WhenIGetTheConfig()) + .Then(x => x.TheFollowingIsReturned( + new ErrorResponse(new List + { + new AnyError() + }))) + .BDDfy(); + } + + [Fact] + public void should_return_error_if_creator_errors() + { + this.Given(x => x.GivenTheRepoReturns(new OkResponse(null))) + .And(x => x.GivenTheCreatorReturns(new ErrorResponse(new List + { + new AnyError() + }))) + .When(x => x.WhenIGetTheConfig()) + .Then(x => x.TheFollowingIsReturned(new ErrorResponse(new List + { + new AnyError() + }))) + .BDDfy(); + } + + private void GivenTheCreatorReturns(Response config) + { + _creator + .Setup(x => x.Create()) + .Returns(config); + } + + private void GivenTheRepoReturns(Response config) + { + _configurationRepository + .Setup(x => x.Get()) + .Returns(config); + } + + private void WhenIGetTheConfig() + { + _result = _ocelotConfigurationProvider.Get(); + } + + private void TheFollowingIsReturned(Response expected) + { + _result.IsError.ShouldBe(expected.IsError); + } + + class AnyError : Error + { + public AnyError() + : base("blamo", OcelotErrorCode.UnknownError) + { + } + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs new file mode 100644 index 00000000..ec46f914 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Repository; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Configuration +{ + public class InMemoryConfigurationRepositoryTests + { + private readonly InMemoryOcelotConfigurationRepository _repo; + private IOcelotConfiguration _config; + private Response _result; + private Response _getResult; + + public InMemoryConfigurationRepositoryTests() + { + _repo = new InMemoryOcelotConfigurationRepository(); + } + + [Fact] + public void can_add_config() + { + this.Given(x => x.GivenTheConfigurationIs(new FakeConfig("initial"))) + .When(x => x.WhenIAddOrReplaceTheConfig()) + .Then(x => x.ThenNoErrorsAreReturned()) + .BDDfy(); + } + + [Fact] + public void can_get_config() + { + this.Given(x => x.GivenThereIsASavedConfiguration()) + .When(x => x.WhenIGetTheConfiguration()) + .Then(x => x.ThenTheConfigurationIsReturned()) + .BDDfy(); + } + + private void ThenTheConfigurationIsReturned() + { + _getResult.Data.ReRoutes[0].DownstreamPathTemplate.Value.ShouldBe("initial"); + } + + private void WhenIGetTheConfiguration() + { + _getResult = _repo.Get(); + } + + private void GivenThereIsASavedConfiguration() + { + GivenTheConfigurationIs(new FakeConfig("initial")); + WhenIAddOrReplaceTheConfig(); + } + + private void GivenTheConfigurationIs(IOcelotConfiguration config) + { + _config = config; + } + + private void WhenIAddOrReplaceTheConfig() + { + _result = _repo.AddOrReplace(_config); + } + + private void ThenNoErrorsAreReturned() + { + _result.IsError.ShouldBeFalse(); + } + + class FakeConfig : IOcelotConfiguration + { + private readonly string _downstreamTemplatePath; + + public FakeConfig(string downstreamTemplatePath) + { + _downstreamTemplatePath = downstreamTemplatePath; + } + + public List ReRoutes => new List + { + new ReRouteBuilder().WithDownstreamPathTemplate(_downstreamTemplatePath).Build() + }; + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs new file mode 100644 index 00000000..0d5a6d48 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.DownstreamRouteFinder.Middleware; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Responses; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamRouteFinder +{ + public class DownstreamRouteFinderMiddlewareTests : IDisposable + { + private readonly Mock _downstreamRouteFinder; + private readonly Mock _scopedRepository; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private Response _downstreamRoute; + private HttpResponseMessage _result; + + public DownstreamRouteFinderMiddlewareTests() + { + _url = "http://localhost:51879"; + _downstreamRouteFinder = new Mock(); + _scopedRepository = new Mock(); + + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_downstreamRouteFinder.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseDownstreamRouteFinderMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_scoped_data_repository_correctly() + { + this.Given(x => x.GivenTheDownStreamRouteFinderReturns(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamPathTemplate("any old string").Build()))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) + .BDDfy(); + } + + + private void ThenTheScopedDataRepositoryIsCalledCorrectly() + { + _scopedRepository + .Verify(x => x.Add("DownstreamRoute", _downstreamRoute.Data), Times.Once()); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheDownStreamRouteFinderReturns(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _downstreamRouteFinder + .Setup(x => x.FindDownstreamRoute(It.IsAny(), It.IsAny())) + .Returns(_downstreamRoute); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs new file mode 100644 index 00000000..c0afca42 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -0,0 +1,191 @@ +using System.Collections.Generic; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.Provider; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.Finder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamRouteFinder +{ + public class DownstreamRouteFinderTests + { + private readonly IDownstreamRouteFinder _downstreamRouteFinder; + private readonly Mock _mockConfig; + private readonly Mock _mockMatcher; + private readonly Mock _finder; + private string _upstreamUrlPath; + private Response _result; + private List _reRoutesConfig; + private Response _match; + private string _upstreamHttpMethod; + + public DownstreamRouteFinderTests() + { + _mockConfig = new Mock(); + _mockMatcher = new Mock(); + _finder = new Mock(); + _downstreamRouteFinder = new Ocelot.DownstreamRouteFinder.Finder.DownstreamRouteFinder(_mockConfig.Object, _mockMatcher.Object, _finder.Object); + } + + [Fact] + public void should_return_route() + { + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And( + x => + x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamTemplate("someUpstreamPath") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("someUpstreamPath") + .Build() + } + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .Build() + ))) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + + [Fact] + public void should_return_correct_route_for_http_verb() + { + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("someUpstreamPath")) + .And( + x => + x.GivenTheTemplateVariableAndNameFinderReturns( + new OkResponse>(new List()))) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPath") + .WithUpstreamTemplate("someUpstreamPath") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("") + .Build(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPathForAPost") + .WithUpstreamTemplate("someUpstreamPath") + .WithUpstreamHttpMethod("Post") + .WithUpstreamTemplatePattern("") + .Build() + } + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(true)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Post")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenTheFollowingIsReturned(new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("someDownstreamPathForAPost") + .Build() + ))) + .BDDfy(); + } + + [Fact] + public void should_not_return_route() + { + this.Given(x => x.GivenThereIsAnUpstreamUrlPath("somePath")) + .And(x => x.GivenTheConfigurationIs(new List + { + new ReRouteBuilder() + .WithDownstreamPathTemplate("somPath") + .WithUpstreamTemplate("somePath") + .WithUpstreamHttpMethod("Get") + .WithUpstreamTemplatePattern("somePath") + .Build(), + } + )) + .And(x => x.GivenTheUrlMatcherReturns(new OkResponse(new UrlMatch(false)))) + .And(x => x.GivenTheUpstreamHttpMethodIs("Get")) + .When(x => x.WhenICallTheFinder()) + .Then( + x => x.ThenAnErrorResponseIsReturned()) + .And(x => x.ThenTheUrlMatcherIsCalledCorrectly()) + .BDDfy(); + } + + private void GivenTheTemplateVariableAndNameFinderReturns(Response> response) + { + _finder + .Setup(x => x.Find(It.IsAny(), It.IsAny())) + .Returns(response); + } + + private void GivenTheUpstreamHttpMethodIs(string upstreamHttpMethod) + { + _upstreamHttpMethod = upstreamHttpMethod; + } + + private void ThenAnErrorResponseIsReturned() + { + _result.IsError.ShouldBeTrue(); + } + + private void ThenTheUrlMatcherIsCalledCorrectly() + { + _mockMatcher + .Verify(x => x.Match(_upstreamUrlPath, _reRoutesConfig[0].UpstreamTemplate), Times.Once); + } + + private void GivenTheUrlMatcherReturns(Response match) + { + _match = match; + _mockMatcher + .Setup(x => x.Match(It.IsAny(), It.IsAny())) + .Returns(_match); + } + + private void GivenTheConfigurationIs(List reRoutesConfig) + { + _reRoutesConfig = reRoutesConfig; + _mockConfig + .Setup(x => x.Get()) + .Returns(new OkResponse(new OcelotConfiguration(_reRoutesConfig))); + } + + private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) + { + _upstreamUrlPath = upstreamUrlPath; + } + + private void WhenICallTheFinder() + { + _result = _downstreamRouteFinder.FindDownstreamRoute(_upstreamUrlPath, _upstreamHttpMethod); + } + + private void ThenTheFollowingIsReturned(DownstreamRoute expected) + { + _result.Data.ReRoute.DownstreamPathTemplate.Value.ShouldBe(expected.ReRoute.DownstreamPathTemplate.Value); + + for (int i = 0; i < _result.Data.TemplatePlaceholderNameAndValues.Count; i++) + { + _result.Data.TemplatePlaceholderNameAndValues[i].TemplateVariableName.ShouldBe( + expected.TemplatePlaceholderNameAndValues[i].TemplateVariableName); + + _result.Data.TemplatePlaceholderNameAndValues[i].TemplateVariableValue.ShouldBe( + expected.TemplatePlaceholderNameAndValues[i].TemplateVariableValue); + } + + _result.IsError.ShouldBeFalse(); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs new file mode 100644 index 00000000..ea069593 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs @@ -0,0 +1,176 @@ +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher +{ + public class RegExUrlMatcherTests + { + private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; + private string _downstreamUrlPath; + private string _downstreamPathTemplate; + private Response _result; + + public RegExUrlMatcherTests() + { + _urlMatcher = new RegExUrlMatcher(); + } + + [Fact] + public void should_find_match_when_template_smaller_than_valid_path() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/api/products/2354325435624623464235")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("/api/products/.*$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_not_find_match() + { + this.Given(x => x.GivenIHaveAUpstreamPath("/api/values")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("/$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url() + { + this.Given(x => x.GivenIHaveAUpstreamPath("")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("$")) + .When(x => x.WhenIMatchThePaths()) + .And(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_no_slash() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_one_slash() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api/product/products/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_one_place_holder() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api/product/products/.*$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_two_place_holders() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/2")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api/product/products/.*/.*$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_two_place_holders_seperated_by_something() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api/product/products/.*/categories/.*$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_three_place_holders_seperated_by_something() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/123")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api/product/products/.*/categories/.*/variant/.*$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_three_place_holders() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api/product/products/.*/categories/.*/variant/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_ignore_case_sensitivity() + { + this.Given(x => x.GivenIHaveAUpstreamPath("API/product/products/1/categories/2/variant/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("(?i)api/product/products/.*/categories/.*/variant/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsTrue()) + .BDDfy(); + } + + [Fact] + public void should_respect_case_sensitivity() + { + this.Given(x => x.GivenIHaveAUpstreamPath("API/product/products/1/categories/2/variant/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplatePattern("api/product/products/.*/categories/.*/variant/$")) + .When(x => x.WhenIMatchThePaths()) + .Then(x => x.ThenTheResultIsFalse()) + .BDDfy(); + } + + private void GivenIHaveAUpstreamPath(string downstreamPath) + { + _downstreamUrlPath = downstreamPath; + } + + private void GivenIHaveAnUpstreamUrlTemplatePattern(string downstreamUrlTemplate) + { + _downstreamPathTemplate = downstreamUrlTemplate; + } + + private void WhenIMatchThePaths() + { + _result = _urlMatcher.Match(_downstreamUrlPath, _downstreamPathTemplate); + } + + private void ThenTheResultIsTrue() + { + _result.Data.Match.ShouldBeTrue(); + } + + private void ThenTheResultIsFalse() + { + _result.Data.Match.ShouldBeFalse(); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/TemplateVariableNameAndValueFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/TemplateVariableNameAndValueFinderTests.cs new file mode 100644 index 00000000..cf71c809 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/TemplateVariableNameAndValueFinderTests.cs @@ -0,0 +1,168 @@ +using System.Collections.Generic; +using System.Linq; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher +{ + public class UrlPathToUrlTemplateMatcherTests + { + private readonly IUrlPathPlaceholderNameAndValueFinder _finder; + private string _downstreamUrlPath; + private string _downstreamPathTemplate; + private Response> _result; + + public UrlPathToUrlTemplateMatcherTests() + { + _finder = new UrlPathPlaceholderNameAndValueFinder(); + } + + [Fact] + public void can_match_down_stream_url() + { + this.Given(x => x.GivenIHaveAUpstreamPath("")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplate("")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(new List())) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_no_slash() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplate("api")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(new List())) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_one_slash() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplate("api/")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(new List())) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template() + { + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplate("api/product/products/")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(new List())) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_one_place_holder() + { + var expectedTemplates = new List + { + new UrlPathPlaceholderNameAndValue("{productId}", "1") + }; + + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplate("api/product/products/{productId}")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_two_place_holders() + { + var expectedTemplates = new List + { + new UrlPathPlaceholderNameAndValue("{productId}", "1"), + new UrlPathPlaceholderNameAndValue("{categoryId}", "2") + }; + + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/2")) + .Given(x => x.GivenIHaveAnUpstreamUrlTemplate("api/product/products/{productId}/{categoryId}")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_two_place_holders_seperated_by_something() + { + var expectedTemplates = new List + { + new UrlPathPlaceholderNameAndValue("{productId}", "1"), + new UrlPathPlaceholderNameAndValue("{categoryId}", "2") + }; + + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplate("api/product/products/{productId}/categories/{categoryId}")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_three_place_holders_seperated_by_something() + { + var expectedTemplates = new List + { + new UrlPathPlaceholderNameAndValue("{productId}", "1"), + new UrlPathPlaceholderNameAndValue("{categoryId}", "2"), + new UrlPathPlaceholderNameAndValue("{variantId}", "123") + }; + + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/123")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplate("api/product/products/{productId}/categories/{categoryId}/variant/{variantId}")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) + .BDDfy(); + } + + [Fact] + public void can_match_down_stream_url_with_downstream_template_with_three_place_holders() + { + var expectedTemplates = new List + { + new UrlPathPlaceholderNameAndValue("{productId}", "1"), + new UrlPathPlaceholderNameAndValue("{categoryId}", "2") + }; + + this.Given(x => x.GivenIHaveAUpstreamPath("api/product/products/1/categories/2/variant/")) + .And(x => x.GivenIHaveAnUpstreamUrlTemplate("api/product/products/{productId}/categories/{categoryId}/variant/")) + .When(x => x.WhenIFindTheUrlVariableNamesAndValues()) + .And(x => x.ThenTheTemplatesVariablesAre(expectedTemplates)) + .BDDfy(); + } + + private void ThenTheTemplatesVariablesAre(List expectedResults) + { + foreach (var expectedResult in expectedResults) + { + var result = _result.Data + .First(t => t.TemplateVariableName == expectedResult.TemplateVariableName); + result.TemplateVariableValue.ShouldBe(expectedResult.TemplateVariableValue); + } + } + + private void GivenIHaveAUpstreamPath(string downstreamPath) + { + _downstreamUrlPath = downstreamPath; + } + + private void GivenIHaveAnUpstreamUrlTemplate(string downstreamUrlTemplate) + { + _downstreamPathTemplate = downstreamUrlTemplate; + } + + private void WhenIFindTheUrlVariableNamesAndValues() + { + _result = _finder.Find(_downstreamUrlPath, _downstreamPathTemplate); + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs new file mode 100644 index 00000000..5581a32e --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.Middleware; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator; +using Ocelot.DownstreamUrlCreator.Middleware; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Responses; +using Ocelot.Values; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamUrlCreator +{ + public class DownstreamUrlCreatorMiddlewareTests : IDisposable + { + private readonly Mock _downstreamUrlTemplateVariableReplacer; + private readonly Mock _scopedRepository; + private readonly Mock _urlBuilder; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private Response _downstreamRoute; + private HttpResponseMessage _result; + private OkResponse _downstreamPath; + private OkResponse _downstreamUrl; + + public DownstreamUrlCreatorMiddlewareTests() + { + _url = "http://localhost:51879"; + _downstreamUrlTemplateVariableReplacer = new Mock(); + _scopedRepository = new Mock(); + _urlBuilder = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_downstreamUrlTemplateVariableReplacer.Object); + x.AddSingleton(_scopedRepository.Object); + x.AddSingleton(_urlBuilder.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseDownstreamUrlCreatorMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_dependencies_correctly() + { + this.Given(x => x.GivenTheDownStreamRouteIs(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamPathTemplate("any old string").Build()))) + .And(x => x.TheUrlReplacerReturns("/api/products/1")) + .And(x => x.TheUrlBuilderReturns("http://www.bbc.co.uk/api/products/1")) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) + .BDDfy(); + } + + private void TheUrlBuilderReturns(string dsUrl) + { + _downstreamUrl = new OkResponse(new DownstreamUrl(dsUrl)); + _urlBuilder + .Setup(x => x.Build(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(_downstreamUrl); + } + + private void TheUrlReplacerReturns(string downstreamUrl) + { + _downstreamPath = new OkResponse(new DownstreamPath(downstreamUrl)); + _downstreamUrlTemplateVariableReplacer + .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) + .Returns(_downstreamPath); + } + + private void ThenTheScopedDataRepositoryIsCalledCorrectly() + { + _scopedRepository + .Verify(x => x.Add("DownstreamUrl", _downstreamUrl.Data.Value), Times.Once()); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlBuilderTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlBuilderTests.cs new file mode 100644 index 00000000..7e512798 --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlBuilderTests.cs @@ -0,0 +1,124 @@ +using System; +using Ocelot.Configuration; +using Ocelot.DownstreamUrlCreator; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamUrlCreator +{ + public class UrlBuilderTests + { + private readonly IUrlBuilder _urlBuilder; + private string _dsPath; + private string _dsScheme; + private string _dsHost; + private int _dsPort; + + private Response _result; + + public UrlBuilderTests() + { + _urlBuilder = new UrlBuilder(); + } + + [Fact] + public void should_return_error_when_downstream_path_is_null() + { + this.Given(x => x.GivenADownstreamPath(null)) + .When(x => x.WhenIBuildTheUrl()) + .Then(x => x.ThenThereIsAnErrorOfType()) + .BDDfy(); + } + + [Fact] + public void should_return_error_when_downstream_scheme_is_null() + { + this.Given(x => x.GivenADownstreamScheme(null)) + .And(x => x.GivenADownstreamPath("test")) + .When(x => x.WhenIBuildTheUrl()) + .Then(x => x.ThenThereIsAnErrorOfType()) + .BDDfy(); + } + + [Fact] + public void should_return_error_when_downstream_host_is_null() + { + this.Given(x => x.GivenADownstreamScheme(null)) + .And(x => x.GivenADownstreamPath("test")) + .And(x => x.GivenADownstreamScheme("test")) + .When(x => x.WhenIBuildTheUrl()) + .Then(x => x.ThenThereIsAnErrorOfType()) + .BDDfy(); + } + + [Fact] + public void should_not_use_port_if_zero() + { + this.Given(x => x.GivenADownstreamPath("/api/products/1")) + .And(x => x.GivenADownstreamScheme("http")) + .And(x => x.GivenADownstreamHost("127.0.0.1")) + .And(x => x.GivenADownstreamPort(0)) + .When(x => x.WhenIBuildTheUrl()) + .Then(x => x.ThenTheUrlIsReturned("http://127.0.0.1/api/products/1")) + .And(x => x.ThenTheUrlIsWellFormed()) + .BDDfy(); + } + + [Fact] + public void should_build_well_formed_uri() + { + this.Given(x => x.GivenADownstreamPath("/api/products/1")) + .And(x => x.GivenADownstreamScheme("http")) + .And(x => x.GivenADownstreamHost("127.0.0.1")) + .And(x => x.GivenADownstreamPort(5000)) + .When(x => x.WhenIBuildTheUrl()) + .Then(x => x.ThenTheUrlIsReturned("http://127.0.0.1:5000/api/products/1")) + .And(x => x.ThenTheUrlIsWellFormed()) + .BDDfy(); + } + + private void ThenThereIsAnErrorOfType() + { + _result.Errors[0].ShouldBeOfType(); + } + + private void GivenADownstreamPath(string dsPath) + { + _dsPath = dsPath; + } + + private void GivenADownstreamScheme(string dsScheme) + { + _dsScheme = dsScheme; + } + + private void GivenADownstreamHost(string dsHost) + { + _dsHost = dsHost; + } + + private void GivenADownstreamPort(int dsPort) + { + _dsPort = dsPort; + } + + private void WhenIBuildTheUrl() + { + _result = _urlBuilder.Build(_dsPath, _dsScheme, new HostAndPort(_dsHost, _dsPort)); + } + + private void ThenTheUrlIsReturned(string expected) + { + _result.Data.Value.ShouldBe(expected); + } + + private void ThenTheUrlIsWellFormed() + { + Uri.IsWellFormedUriString(_result.Data.Value, UriKind.Absolute).ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs new file mode 100644 index 00000000..a7a5a89b --- /dev/null +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.Responses; +using Ocelot.Values; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer +{ + public class UpstreamUrlPathTemplateVariableReplacerTests + { + private DownstreamRoute _downstreamRoute; + private Response _result; + private readonly IDownstreamPathPlaceholderReplacer _downstreamPathReplacer; + + public UpstreamUrlPathTemplateVariableReplacerTests() + { + _downstreamPathReplacer = new DownstreamTemplatePathPlaceholderReplacer(); + } + + [Fact] + public void can_replace_no_template_variables() + { + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder().Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("")) + .BDDfy(); + } + + [Fact] + public void can_replace_no_template_variables_with_slash() + { + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamPathTemplate("/").Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("/")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_no_slash() + { + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamPathTemplate("api").Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_one_slash() + { + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamPathTemplate("api/").Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api/")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_multiple_slash() + { + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(new List(), new ReRouteBuilder().WithDownstreamPathTemplate("api/product/products/").Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("api/product/products/")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_one_template_variable() + { + var templateVariables = new List() + { + new UrlPathPlaceholderNameAndValue("{productId}", "1") + }; + + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, new ReRouteBuilder().WithDownstreamPathTemplate("productservice/products/{productId}/").Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_one_template_variable_with_path_after() + { + var templateVariables = new List() + { + new UrlPathPlaceholderNameAndValue("{productId}", "1") + }; + + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, new ReRouteBuilder().WithDownstreamPathTemplate("productservice/products/{productId}/variants").Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/variants")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_two_template_variable() + { + var templateVariables = new List() + { + new UrlPathPlaceholderNameAndValue("{productId}", "1"), + new UrlPathPlaceholderNameAndValue("{variantId}", "12") + }; + + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, new ReRouteBuilder().WithDownstreamPathTemplate("productservice/products/{productId}/variants/{variantId}").Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/products/1/variants/12")) + .BDDfy(); + } + + [Fact] + public void can_replace_url_three_template_variable() + { + var templateVariables = new List() + { + new UrlPathPlaceholderNameAndValue("{productId}", "1"), + new UrlPathPlaceholderNameAndValue("{variantId}", "12"), + new UrlPathPlaceholderNameAndValue("{categoryId}", "34") + }; + + this.Given(x => x.GivenThereIsAUrlMatch(new DownstreamRoute(templateVariables, new ReRouteBuilder().WithDownstreamPathTemplate("productservice/category/{categoryId}/products/{productId}/variants/{variantId}").Build()))) + .When(x => x.WhenIReplaceTheTemplateVariables()) + .Then(x => x.ThenTheDownstreamUrlPathIsReturned("productservice/category/34/products/1/variants/12")) + .BDDfy(); + } + + private void GivenThereIsAUrlMatch(DownstreamRoute downstreamRoute) + { + _downstreamRoute = downstreamRoute; + } + + private void WhenIReplaceTheTemplateVariables() + { + _result = _downstreamPathReplacer.Replace(_downstreamRoute.ReRoute.DownstreamPathTemplate, _downstreamRoute.TemplatePlaceholderNameAndValues); + } + + private void ThenTheDownstreamUrlPathIsReturned(string expected) + { + _result.Data.Value.ShouldBe(expected); + } + } +} diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestTests.cs new file mode 100644 index 00000000..47951859 --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestTests.cs @@ -0,0 +1,154 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Moq; +using Ocelot.Configuration; +using Ocelot.Errors; +using Ocelot.Headers; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Headers +{ + public class AddHeadersToRequestTests + { + private readonly AddHeadersToRequest _addHeadersToRequest; + private readonly Mock _parser; + private List _configuration; + private HttpContext _context; + private Response _result; + private Response _claimValue; + + public AddHeadersToRequestTests() + { + _parser = new Mock(); + _addHeadersToRequest = new AddHeadersToRequest(_parser.Object); + } + + [Fact] + public void should_add_headers_to_context() + { + var context = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim("test", "data") + })) + }; + + this.Given( + x => x.GivenConfigurationHeaderExtractorProperties(new List + { + new ClaimToThing("header-key", "", "", 0) + })) + .Given(x => x.GivenHttpContext(context)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddHeadersToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenTheHeaderIsAdded()) + .BDDfy(); + } + + [Fact] + public void if_header_exists_should_replace_it() + { + var context = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim("test", "data") + })), + }; + + context.Request.Headers.Add("header-key", new StringValues("initial")); + + this.Given( + x => x.GivenConfigurationHeaderExtractorProperties(new List + { + new ClaimToThing("header-key", "", "", 0) + })) + .Given(x => x.GivenHttpContext(context)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddHeadersToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenTheHeaderIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + this.Given( + x => x.GivenConfigurationHeaderExtractorProperties(new List + { + new ClaimToThing("", "", "", 0) + })) + .Given(x => x.GivenHttpContext(new DefaultHttpContext())) + .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List + { + new AnyError() + }))) + .When(x => x.WhenIAddHeadersToTheRequest()) + .Then(x => x.ThenTheResultIsError()) + .BDDfy(); + } + + private void ThenTheHeaderIsAdded() + { + var header = _context.Request.Headers.First(x => x.Key == "header-key"); + header.Value.First().ShouldBe(_claimValue.Data); + } + + private void GivenConfigurationHeaderExtractorProperties(List configuration) + { + _configuration = configuration; + } + + private void GivenHttpContext(HttpContext context) + { + _context = context; + } + + private void GivenTheClaimParserReturns(Response claimValue) + { + _claimValue = claimValue; + _parser + .Setup( + x => + x.GetValue(It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(_claimValue); + } + + private void WhenIAddHeadersToTheRequest() + { + _result = _addHeadersToRequest.SetHeadersOnContext(_configuration, _context); + } + + private void ThenTheResultIsSuccess() + { + _result.IsError.ShouldBe(false); + } + + private void ThenTheResultIsError() + { + + _result.IsError.ShouldBe(true); + } + + class AnyError : Error + { + public AnyError() + : base("blahh", OcelotErrorCode.UnknownError) + { + } + } + } +} diff --git a/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs new file mode 100644 index 00000000..3516d26b --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator.Middleware; +using Ocelot.Headers; +using Ocelot.Headers.Middleware; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Responses; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Headers +{ + public class HttpRequestHeadersBuilderMiddlewareTests : IDisposable + { + private readonly Mock _scopedRepository; + private readonly Mock _addHeaders; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private Response _downstreamRoute; + private HttpResponseMessage _result; + + public HttpRequestHeadersBuilderMiddlewareTests() + { + _url = "http://localhost:51879"; + _scopedRepository = new Mock(); + _addHeaders = new Mock(); + + + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_addHeaders.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseHttpRequestHeadersBuilderMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_add_headers_to_request_correctly() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithClaimsToHeaders(new List + { + new ClaimToThing("UserId", "Subject", "", 0) + }) + .Build()); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheAddHeadersToRequestReturns()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheAddHeadersToRequestIsCalledCorrectly()) + .BDDfy(); + } + + private void GivenTheAddHeadersToRequestReturns() + { + _addHeaders + .Setup(x => x.SetHeadersOnContext(It.IsAny>(), + It.IsAny())) + .Returns(new OkResponse()); + } + + private void ThenTheAddHeadersToRequestIsCalledCorrectly() + { + _addHeaders + .Verify(x => x.SetHeadersOnContext(It.IsAny>(), + It.IsAny()), Times.Once); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs new file mode 100644 index 00000000..7a7bd4cc --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs @@ -0,0 +1,52 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Headers +{ + public class RemoveHeadersTests + { + private HttpResponseHeaders _headers; + private readonly Ocelot.Headers.RemoveOutputHeaders _removeOutputHeaders; + private Response _result; + + public RemoveHeadersTests() + { + _removeOutputHeaders = new Ocelot.Headers.RemoveOutputHeaders(); + } + + [Fact] + public void should_remove_header() + { + var httpResponse = new HttpResponseMessage() + { + Headers = {{ "Transfer-Encoding", "chunked"}} + }; + + this.Given(x => x.GivenAHttpContext(httpResponse.Headers)) + .When(x => x.WhenIRemoveTheHeaders()) + .Then(x => x.TheHeaderIsNoLongerInTheContext()) + .BDDfy(); + } + + private void GivenAHttpContext(HttpResponseHeaders headers) + { + _headers = headers; + } + + private void WhenIRemoveTheHeaders() + { + _result = _removeOutputHeaders.Remove(_headers); + } + + private void TheHeaderIsNoLongerInTheContext() + { + _result.IsError.ShouldBeFalse(); + _headers.ShouldNotContain(x => x.Key == "Transfer-Encoding"); + _headers.ShouldNotContain(x => x.Key == "transfer-encoding"); + } + } +} diff --git a/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs b/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs new file mode 100644 index 00000000..2358166f --- /dev/null +++ b/test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs @@ -0,0 +1,124 @@ +using Ocelot.Errors; + +namespace Ocelot.UnitTests.Infrastructure +{ + using System.Collections.Generic; + using System.Security.Claims; + using Ocelot.Infrastructure.Claims.Parser; + using Responses; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + + public class ClaimParserTests + { + private readonly IClaimsParser _claimsParser; + private readonly List _claims; + private string _key; + private Response _result; + private string _delimiter; + private int _index; + + public ClaimParserTests() + { + _claims = new List(); + _claimsParser = new ClaimsParser(); + } + + [Fact] + public void can_parse_claims_dictionary_access_string_returning_value_to_function() + { + this.Given(x => x.GivenAClaimOf(new Claim("CustomerId", "1234"))) + .And(x => x.GivenTheKeyIs("CustomerId")) + .When(x => x.WhenICallTheParser()) + .Then(x => x.ThenTheResultIs(new OkResponse("1234"))) + .BDDfy(); + } + + [Fact] + public void should_return_error_response_when_cannot_find_requested_claim() + { + this.Given(x => x.GivenAClaimOf(new Claim("BallsId", "1234"))) + .And(x => x.GivenTheKeyIs("CustomerId")) + .When(x => x.WhenICallTheParser()) + .Then(x => x.ThenTheResultIs(new ErrorResponse(new List + { + new CannotFindClaimError($"Cannot find claim for key: {_key}") + }))) + .BDDfy(); + } + + [Fact] + public void can_parse_claims_dictionary_access_string_using_delimiter_and_retuning_at_correct_index() + { + this.Given(x => x.GivenAClaimOf(new Claim("Subject", "registered|4321"))) + .And(x => x.GivenTheDelimiterIs("|")) + .And(x => x.GivenTheIndexIs(1)) + .And(x => x.GivenTheKeyIs("Subject")) + .When(x => x.WhenICallTheParser()) + .Then(x => x.ThenTheResultIs(new OkResponse("4321"))) + .BDDfy(); + } + + [Fact] + public void should_return_error_response_if_index_too_large() + { + this.Given(x => x.GivenAClaimOf(new Claim("Subject", "registered|4321"))) + .And(x => x.GivenTheDelimiterIs("|")) + .And(x => x.GivenTheIndexIs(24)) + .And(x => x.GivenTheKeyIs("Subject")) + .When(x => x.WhenICallTheParser()) + .Then(x => x.ThenTheResultIs(new ErrorResponse(new List + { + new CannotFindClaimError($"Cannot find claim for key: {_key}, delimiter: {_delimiter}, index: {_index}") + }))) + .BDDfy(); + } + + [Fact] + public void should_return_error_response_if_index_too_small() + { + this.Given(x => x.GivenAClaimOf(new Claim("Subject", "registered|4321"))) + .And(x => x.GivenTheDelimiterIs("|")) + .And(x => x.GivenTheIndexIs(-1)) + .And(x => x.GivenTheKeyIs("Subject")) + .When(x => x.WhenICallTheParser()) + .Then(x => x.ThenTheResultIs(new ErrorResponse(new List + { + new CannotFindClaimError($"Cannot find claim for key: {_key}, delimiter: {_delimiter}, index: {_index}") + }))) + .BDDfy(); + } + + private void GivenTheIndexIs(int index) + { + _index = index; + } + + private void GivenTheDelimiterIs(string delimiter) + { + _delimiter = delimiter; + } + + private void GivenAClaimOf(Claim claim) + { + _claims.Add(claim); + } + + private void GivenTheKeyIs(string key) + { + _key = key; + } + + private void WhenICallTheParser() + { + _result = _claimsParser.GetValue(_claims, _key, _delimiter, _index); + } + + private void ThenTheResultIs(Response expected) + { + _result.Data.ShouldBe(expected.Data); + _result.IsError.ShouldBe(expected.IsError); + } + } +} diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.xproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.xproj new file mode 100644 index 00000000..0936ae02 --- /dev/null +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.xproj @@ -0,0 +1,22 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 54e84f1a-e525-4443-96ec-039cbd50c263 + Ocelot.UnitTests + .\obj + .\bin\ + v4.5 + + + 2.0 + + + + + + \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Properties/AssemblyInfo.cs b/test/Ocelot.UnitTests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..a2be759e --- /dev/null +++ b/test/Ocelot.UnitTests/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Ocelot.UnitTests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("54e84f1a-e525-4443-96ec-039cbd50c263")] diff --git a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs new file mode 100644 index 00000000..99cf68b8 --- /dev/null +++ b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs @@ -0,0 +1,153 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Configuration; +using Ocelot.Errors; +using Ocelot.Infrastructure.Claims.Parser; +using Ocelot.QueryStrings; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.QueryStrings +{ + public class AddQueriesToRequestTests + { + private readonly AddQueriesToRequest _addQueriesToRequest; + private readonly Mock _parser; + private List _configuration; + private HttpContext _context; + private Response _result; + private Response _claimValue; + + public AddQueriesToRequestTests() + { + _parser = new Mock(); + _addQueriesToRequest = new AddQueriesToRequest(_parser.Object); + } + + [Fact] + public void should_add_queries_to_context() + { + var context = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim("test", "data") + })) + }; + + this.Given( + x => x.GivenAClaimToThing(new List + { + new ClaimToThing("query-key", "", "", 0) + })) + .Given(x => x.GivenHttpContext(context)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddQueriesToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenTheQueryIsAdded()) + .BDDfy(); + } + + [Fact] + public void if_query_exists_should_replace_it() + { + var context = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim("test", "data") + })), + }; + + context.Request.QueryString = context.Request.QueryString.Add("query-key", "initial"); + + this.Given( + x => x.GivenAClaimToThing(new List + { + new ClaimToThing("query-key", "", "", 0) + })) + .Given(x => x.GivenHttpContext(context)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddQueriesToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenTheQueryIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_return_error() + { + this.Given( + x => x.GivenAClaimToThing(new List + { + new ClaimToThing("", "", "", 0) + })) + .Given(x => x.GivenHttpContext(new DefaultHttpContext())) + .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List + { + new AnyError() + }))) + .When(x => x.WhenIAddQueriesToTheRequest()) + .Then(x => x.ThenTheResultIsError()) + .BDDfy(); + } + + private void ThenTheQueryIsAdded() + { + var query = _context.Request.Query.First(x => x.Key == "query-key"); + query.Value.First().ShouldBe(_claimValue.Data); + } + + private void GivenAClaimToThing(List configuration) + { + _configuration = configuration; + } + + private void GivenHttpContext(HttpContext context) + { + _context = context; + } + + private void GivenTheClaimParserReturns(Response claimValue) + { + _claimValue = claimValue; + _parser + .Setup( + x => + x.GetValue(It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(_claimValue); + } + + private void WhenIAddQueriesToTheRequest() + { + _result = _addQueriesToRequest.SetQueriesOnContext(_configuration, _context); + } + + private void ThenTheResultIsSuccess() + { + _result.IsError.ShouldBe(false); + } + + private void ThenTheResultIsError() + { + + _result.IsError.ShouldBe(true); + } + + class AnyError : Error + { + public AnyError() + : base("blahh", OcelotErrorCode.UnknownError) + { + } + } + } +} diff --git a/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs new file mode 100644 index 00000000..39b32937 --- /dev/null +++ b/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Headers.Middleware; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.QueryStrings; +using Ocelot.QueryStrings.Middleware; +using Ocelot.Responses; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.QueryStrings +{ + public class QueryStringBuilderMiddlewareTests : IDisposable + { + private readonly Mock _scopedRepository; + private readonly Mock _addQueries; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private Response _downstreamRoute; + private HttpResponseMessage _result; + + public QueryStringBuilderMiddlewareTests() + { + _url = "http://localhost:51879"; + _scopedRepository = new Mock(); + _addQueries = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_addQueries.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseQueryStringBuilderMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_add_queries_correctly() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithClaimsToQueries(new List + { + new ClaimToThing("UserId", "Subject", "", 0) + }) + .Build()); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheAddHeadersToRequestReturns()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheAddQueriesToRequestIsCalledCorrectly()) + .BDDfy(); + } + + private void GivenTheAddHeadersToRequestReturns() + { + _addQueries + .Setup(x => x.SetQueriesOnContext(It.IsAny>(), + It.IsAny())) + .Returns(new OkResponse()); + } + + private void ThenTheAddQueriesToRequestIsCalledCorrectly() + { + _addQueries + .Verify(x => x.SetQueriesOnContext(It.IsAny>(), + It.IsAny()), Times.Once); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs b/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs new file mode 100644 index 00000000..030c3834 --- /dev/null +++ b/test/Ocelot.UnitTests/Repository/ScopedRequestDataRepositoryTests.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Repository +{ + public class ScopedRequestDataRepositoryTests + { + private IRequestScopedDataRepository _requestScopedDataRepository; + private IHttpContextAccessor _httpContextAccesor; + private string _key; + private object _toAdd; + private Response _result; + + public ScopedRequestDataRepositoryTests() + { + _httpContextAccesor = new HttpContextAccessor(); + _httpContextAccesor.HttpContext = new DefaultHttpContext(); + _requestScopedDataRepository = new HttpDataRepository(_httpContextAccesor); + } + + [Fact] + public void should_add_item() + { + this.Given(x => x.GivenIHaveAnItemToAdd("blahh", new [] {1,2,3,4})) + .When(x => x.WhenIAddTheItem()) + .Then(x => x.ThenTheItemIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_get_item() + { + this.Given(x => x.GivenThereIsAnItemInTheContext("chest")) + .When(x => x.WhenIGetTheItem()) + .Then(x => x.ThenTheItemIsReturned()) + .BDDfy(); + } + + private void ThenTheItemIsReturned() + { + _result.IsError.ShouldBeFalse(); + _result.Data.ShouldNotBeNull(); + } + + private void WhenIGetTheItem() + { + _result = _requestScopedDataRepository.Get(_key); + } + + private void GivenThereIsAnItemInTheContext(string key) + { + _key = key; + var data = new[] {5435345}; + _httpContextAccesor.HttpContext.Items.Add(key, data); + } + + private void GivenIHaveAnItemToAdd(string key, object toAdd) + { + _key = key; + _toAdd = toAdd; + } + + private void WhenIAddTheItem() + { + _requestScopedDataRepository.Add(_key, _toAdd); + } + + private void ThenTheItemIsAdded() + { + object obj; + _httpContextAccesor.HttpContext.Items.TryGetValue(_key, out obj).ShouldBeTrue(); + } + } +} diff --git a/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs new file mode 100644 index 00000000..e2f81abe --- /dev/null +++ b/test/Ocelot.UnitTests/Request/HttpRequestBuilderMiddlewareTests.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Request.Builder; +using Ocelot.Request.Middleware; +using Ocelot.Responses; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Request +{ + public class HttpRequestBuilderMiddlewareTests : IDisposable + { + private readonly Mock _requestBuilder; + private readonly Mock _scopedRepository; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private HttpResponseMessage _result; + private OkResponse _request; + private OkResponse _downstreamUrl; + private OkResponse _downstreamRoute; + + public HttpRequestBuilderMiddlewareTests() + { + _url = "http://localhost:51879"; + _requestBuilder = new Mock(); + _scopedRepository = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_requestBuilder.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseHttpRequestBuilderMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_scoped_data_repository_correctly() + { + + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithRequestIdKey("LSRequestId").Build()); + + + this.Given(x => x.GivenTheDownStreamUrlIs("any old string")) + .And(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheRequestBuilderReturns(new Ocelot.Request.Request(new HttpRequestMessage(), new CookieContainer()))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheScopedDataRepositoryIsCalledCorrectly()) + .BDDfy(); + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + private void GivenTheRequestBuilderReturns(Ocelot.Request.Request request) + { + _request = new OkResponse(request); + _requestBuilder + .Setup(x => x.Build(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(_request); + } + + private void ThenTheScopedDataRepositoryIsCalledCorrectly() + { + _scopedRepository + .Verify(x => x.Add("Request", _request.Data), Times.Once()); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheDownStreamUrlIs(string downstreamUrl) + { + _downstreamUrl = new OkResponse(downstreamUrl); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamUrl); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs b/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs new file mode 100644 index 00000000..3a07532e --- /dev/null +++ b/test/Ocelot.UnitTests/Request/RequestBuilderTests.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Internal; +using Ocelot.Request.Builder; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Request +{ + public class RequestBuilderTests + { + private string _httpMethod; + private string _downstreamUrl; + private HttpContent _content; + private IHeaderDictionary _headers; + private IRequestCookieCollection _cookies; + private QueryString _query; + private string _contentType; + private readonly IRequestCreator _requestCreator; + private Response _result; + private Ocelot.RequestId.RequestId _requestId; + + public RequestBuilderTests() + { + _content = new StringContent(string.Empty); + _requestCreator = new HttpRequestCreator(); + } + + [Fact] + public void should_user_downstream_url() + { + this.Given(x => x.GivenIHaveHttpMethod("GET")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheCorrectDownstreamUrlIsUsed("http://www.bbc.co.uk/")) + .BDDfy(); + } + + [Fact] + public void should_use_http_method() + { + this.Given(x => x.GivenIHaveHttpMethod("POST")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheCorrectHttpMethodIsUsed(HttpMethod.Post)) + .BDDfy(); + } + + [Fact] + public void should_use_http_content() + { + this.Given(x => x.GivenIHaveHttpMethod("POST")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenIHaveTheHttpContent(new StringContent("Hi from Tom"))) + .And(x => x.GivenTheContentTypeIs("application/json")) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheCorrectContentIsUsed(new StringContent("Hi from Tom"))) + .BDDfy(); + } + + [Fact] + public void should_use_http_content_headers() + { + this.Given(x => x.GivenIHaveHttpMethod("POST")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenIHaveTheHttpContent(new StringContent("Hi from Tom"))) + .And(x => x.GivenTheContentTypeIs("application/json")) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheCorrectContentHeadersAreUsed(new HeaderDictionary + { + { + "Content-Type", "application/json" + } + })) + .BDDfy(); + } + + [Fact] + public void should_use_unvalidated_http_content_headers() + { + this.Given(x => x.GivenIHaveHttpMethod("POST")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenIHaveTheHttpContent(new StringContent("Hi from Tom"))) + .And(x => x.GivenTheContentTypeIs("application/json; charset=utf-8")) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheCorrectContentHeadersAreUsed(new HeaderDictionary + { + { + "Content-Type", "application/json; charset=utf-8" + } + })) + .BDDfy(); + } + + [Fact] + public void should_use_headers() + { + this.Given(x => x.GivenIHaveHttpMethod("GET")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenTheHttpHeadersAre(new HeaderDictionary + { + {"ChopSticks", "Bubbles" } + })) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheCorrectHeadersAreUsed(new HeaderDictionary + { + {"ChopSticks", "Bubbles" } + })) + .BDDfy(); + } + + [Fact] + public void should_use_request_id() + { + var requestId = Guid.NewGuid().ToString(); + + this.Given(x => x.GivenIHaveHttpMethod("GET")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenTheHttpHeadersAre(new HeaderDictionary())) + .And(x => x.GivenTheRequestIdIs(new Ocelot.RequestId.RequestId("RequestId", requestId))) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheCorrectHeadersAreUsed(new HeaderDictionary + { + {"RequestId", requestId } + })) + .BDDfy(); + } + + [Fact] + public void should_not_use_request_if_if_already_in_headers() + { + this.Given(x => x.GivenIHaveHttpMethod("GET")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenTheHttpHeadersAre(new HeaderDictionary + { + {"RequestId", "534534gv54gv45g" } + })) + .And(x => x.GivenTheRequestIdIs(new Ocelot.RequestId.RequestId("RequestId", Guid.NewGuid().ToString()))) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheCorrectHeadersAreUsed(new HeaderDictionary + { + {"RequestId", "534534gv54gv45g" } + })) + .BDDfy(); + } + + [Theory] + [InlineData(null, "blahh")] + [InlineData("", "blahh")] + [InlineData("RequestId", "")] + [InlineData("RequestId", null)] + public void should_not_use_request_id(string requestIdKey, string requestIdValue) + { + this.Given(x => x.GivenIHaveHttpMethod("GET")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenTheHttpHeadersAre(new HeaderDictionary())) + .And(x => x.GivenTheRequestIdIs(new Ocelot.RequestId.RequestId(requestIdKey, requestIdValue))) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheRequestIdIsNotInTheHeaders()) + .BDDfy(); + } + + private void GivenTheRequestIdIs(Ocelot.RequestId.RequestId requestId) + { + _requestId = requestId; + } + + [Fact] + public void should_use_cookies() + { + this.Given(x => x.GivenIHaveHttpMethod("GET")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenTheCookiesAre(new RequestCookieCollection(new Dictionary + { + { "TheCookie","Monster" } + }))) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheCorrectCookiesAreUsed(new RequestCookieCollection(new Dictionary + { + { "TheCookie","Monster" } + }))) + .BDDfy(); + } + + [Fact] + public void should_user_query_string() + { + this.Given(x => x.GivenIHaveHttpMethod("POST")) + .And(x => x.GivenIHaveDownstreamUrl("http://www.bbc.co.uk")) + .And(x => x.GivenTheQueryStringIs(new QueryString("?jeff=1&geoff=2"))) + .When(x => x.WhenICreateARequest()) + .And(x => x.ThenTheCorrectQueryStringIsUsed("?jeff=1&geoff=2")) + .BDDfy(); + } + + private void GivenTheContentTypeIs(string contentType) + { + _contentType = contentType; + } + + private void ThenTheCorrectQueryStringIsUsed(string expected) + { + _result.Data.HttpRequestMessage.RequestUri.Query.ShouldBe(expected); + } + + private void GivenTheQueryStringIs(QueryString query) + { + _query = query; + } + + private void ThenTheCorrectCookiesAreUsed(IRequestCookieCollection expected) + { + var resultCookies = _result.Data.CookieContainer.GetCookies(new Uri(_downstreamUrl + _query)); + var resultDictionary = resultCookies.Cast().ToDictionary(cook => cook.Name, cook => cook.Value); + + foreach (var expectedCookie in expected) + { + var resultCookie = resultDictionary[expectedCookie.Key]; + resultCookie.ShouldBe(expectedCookie.Value); + } + } + + private void GivenTheCookiesAre(IRequestCookieCollection cookies) + { + _cookies = cookies; + } + + private void ThenTheRequestIdIsNotInTheHeaders() + { + _result.Data.HttpRequestMessage.Headers.ShouldNotContain(x => x.Key == "RequestId"); + } + + private void ThenTheCorrectHeadersAreUsed(IHeaderDictionary expected) + { + var expectedHeaders = expected.Select(x => new KeyValuePair(x.Key, x.Value)); + + foreach (var expectedHeader in expectedHeaders) + { + _result.Data.HttpRequestMessage.Headers.ShouldContain(x => x.Key == expectedHeader.Key && x.Value.First() == expectedHeader.Value[0]); + } + } + + private void ThenTheCorrectContentHeadersAreUsed(IHeaderDictionary expected) + { + var expectedHeaders = expected.Select(x => new KeyValuePair(x.Key, x.Value)); + + foreach (var expectedHeader in expectedHeaders) + { + _result.Data.HttpRequestMessage.Content.Headers.ShouldContain(x => x.Key == expectedHeader.Key + && x.Value.First() == expectedHeader.Value[0] + ); + } + } + + private void GivenTheHttpHeadersAre(IHeaderDictionary headers) + { + _headers = headers; + } + + private void GivenIHaveTheHttpContent(HttpContent content) + { + _content = content; + } + + private void GivenIHaveHttpMethod(string httpMethod) + { + _httpMethod = httpMethod; + } + + private void GivenIHaveDownstreamUrl(string downstreamUrl) + { + _downstreamUrl = downstreamUrl; + } + + private void WhenICreateARequest() + { + _result = _requestCreator.Build(_httpMethod, _downstreamUrl, _content?.ReadAsStreamAsync().Result, _headers, + _cookies, _query, _contentType, _requestId).Result; + } + + + private void ThenTheCorrectDownstreamUrlIsUsed(string expected) + { + _result.Data.HttpRequestMessage.RequestUri.AbsoluteUri.ShouldBe(expected); + } + + private void ThenTheCorrectHttpMethodIsUsed(HttpMethod expected) + { + _result.Data.HttpRequestMessage.Method.Method.ShouldBe(expected.Method); + } + + private void ThenTheCorrectContentIsUsed(HttpContent expected) + { + _result.Data.HttpRequestMessage.Content.ReadAsStringAsync().Result.ShouldBe(expected.ReadAsStringAsync().Result); + } + } +} diff --git a/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs new file mode 100644 index 00000000..543613a8 --- /dev/null +++ b/test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Configuration.Builder; +using Ocelot.DownstreamRouteFinder; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Request.Middleware; +using Ocelot.RequestId.Middleware; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.RequestId +{ + public class RequestIdMiddlewareTests + { + private readonly Mock _scopedRepository; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private Response _downstreamRoute; + private HttpResponseMessage _result; + private string _value; + private string _key; + + public RequestIdMiddlewareTests() + { + _url = "http://localhost:51879"; + _scopedRepository = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseRequestIdMiddleware(); + + app.Run(x => + { + x.Response.Headers.Add("LSRequestId", x.TraceIdentifier); + return Task.CompletedTask; + }); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_add_request_id_to_repository() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId").Build()); + + var requestId = Guid.NewGuid().ToString(); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheTraceIdIs(requestId)) + .BDDfy(); + } + + [Fact] + public void should_add_trace_indentifier_to_repository() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId").Build()); + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheTraceIdIsAnything()) + .BDDfy(); + } + + private void ThenTheTraceIdIsAnything() + { + _result.Headers.GetValues("LSRequestId").First().ShouldNotBeNullOrEmpty(); + } + + private void ThenTheTraceIdIs(string expected) + { + _result.Headers.GetValues("LSRequestId").First().ShouldBe(expected); + } + + private void GivenTheRequestIdIsAddedToTheRequest(string key, string value) + { + _key = key; + _value = value; + _client.DefaultRequestHeaders.TryAddWithoutValidation(_key, _value); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) + { + _downstreamRoute = new OkResponse(downstreamRoute); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_downstreamRoute); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs new file mode 100644 index 00000000..d99a99eb --- /dev/null +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -0,0 +1,112 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.QueryStrings.Middleware; +using Ocelot.Requester; +using Ocelot.Requester.Middleware; +using Ocelot.Responder; +using Ocelot.Responses; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Requester +{ + public class HttpRequesterMiddlewareTests : IDisposable + { + private readonly Mock _requester; + private readonly Mock _scopedRepository; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private HttpResponseMessage _result; + private OkResponse _response; + private OkResponse _request; + + public HttpRequesterMiddlewareTests() + { + _url = "http://localhost:51879"; + _requester = new Mock(); + _scopedRepository = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_requester.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseHttpRequesterMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_call_scoped_data_repository_correctly() + { + this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage(),new CookieContainer()))) + .And(x => x.GivenTheRequesterReturns(new HttpResponseMessage())) + .And(x => x.GivenTheScopedRepoReturns()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheScopedRepoIsCalledCorrectly()) + .BDDfy(); + } + + private void GivenTheRequesterReturns(HttpResponseMessage response) + { + _response = new OkResponse(response); + _requester + .Setup(x => x.GetResponse(It.IsAny())) + .ReturnsAsync(_response); + } + + private void GivenTheScopedRepoReturns() + { + _scopedRepository + .Setup(x => x.Add(It.IsAny(), _response.Data)) + .Returns(new OkResponse()); + } + + private void ThenTheScopedRepoIsCalledCorrectly() + { + _scopedRepository + .Verify(x => x.Add("HttpResponseMessage", _response.Data), Times.Once()); + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheRequestIs(Ocelot.Request.Request request) + { + _request = new OkResponse(request); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_request); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs new file mode 100644 index 00000000..78f78235 --- /dev/null +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Http; +using Ocelot.Errors; +using Ocelot.Middleware; +using Ocelot.Responder; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Responder +{ + public class ErrorsToHttpStatusCodeMapperTests + { + private readonly IErrorsToHttpStatusCodeMapper _codeMapper; + private Response _result; + private List _errors; + + public ErrorsToHttpStatusCodeMapperTests() + { + _codeMapper = new ErrorsToHttpStatusCodeMapper(); + } + + [Fact] + public void should_create_unauthenticated_response_code() + { + this.Given(x => x.GivenThereAreErrors(new List + { + new UnauthenticatedError("no matter") + })) + .When(x => x.WhenIGetErrorStatusCode()) + .Then(x => x.ThenTheResponseIsStatusCodeIs(401)) + .BDDfy(); + } + + [Fact] + public void should_create_not_found_response_response_code() + { + this.Given(x => x.GivenThereAreErrors(new List + { + new AnyError() + })) + .When(x => x.WhenIGetErrorStatusCode()) + .Then(x => x.ThenTheResponseIsStatusCodeIs(404)) + .BDDfy(); + } + + class AnyError : Error + { + public AnyError() : base("blahh", OcelotErrorCode.UnknownError) + { + } + } + + private void GivenThereAreErrors(List errors) + { + _errors = errors; + } + + private void WhenIGetErrorStatusCode() + { + _result = _codeMapper.Map(_errors); + } + + private void ThenTheResponseIsStatusCodeIs(int expectedCode) + { + _result.Data.ShouldBe(expectedCode); + } + } +} diff --git a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs new file mode 100644 index 00000000..b643028e --- /dev/null +++ b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.Net.Http; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Responder; +using Ocelot.Responder.Middleware; +using Ocelot.Responses; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Responder +{ + public class ResponderMiddlewareTests : IDisposable + { + private readonly Mock _responder; + private readonly Mock _scopedRepository; + private readonly Mock _codeMapper; + private readonly string _url; + private readonly TestServer _server; + private readonly HttpClient _client; + private HttpResponseMessage _result; + private OkResponse _response; + + public ResponderMiddlewareTests() + { + _url = "http://localhost:51879"; + _responder = new Mock(); + _scopedRepository = new Mock(); + _codeMapper = new Mock(); + var builder = new WebHostBuilder() + .ConfigureServices(x => + { + x.AddSingleton(); + x.AddLogging(); + x.AddSingleton(_codeMapper.Object); + x.AddSingleton(_responder.Object); + x.AddSingleton(_scopedRepository.Object); + }) + .UseUrls(_url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(_url) + .Configure(app => + { + app.UseResponderMiddleware(); + }); + + _server = new TestServer(builder); + _client = _server.CreateClient(); + } + + [Fact] + public void should_not_return_any_errors() + { + this.Given(x => x.GivenTheHttpResponseMessageIs(new HttpResponseMessage())) + .And(x => x.GivenThereAreNoPipelineErrors()) + .And(x => x.GivenTheResponderReturns()) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenThereAreNoErrors()) + .BDDfy(); + } + + private void GivenTheResponderReturns() + { + _responder + .Setup(x => x.SetResponseOnHttpContext(It.IsAny(), It.IsAny())) + .ReturnsAsync(new OkResponse()); + } + + private void GivenThereAreNoPipelineErrors() + { + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(new OkResponse(false)); + } + + private void ThenThereAreNoErrors() + { + //todo a better assert? + } + + private void WhenICallTheMiddleware() + { + _result = _client.GetAsync(_url).Result; + } + + private void GivenTheHttpResponseMessageIs(HttpResponseMessage response) + { + _response = new OkResponse(response); + _scopedRepository + .Setup(x => x.Get(It.IsAny())) + .Returns(_response); + } + + public void Dispose() + { + _client.Dispose(); + _server.Dispose(); + } + } +} diff --git a/test/Ocelot.UnitTests/RouterMiddlwareTests.cs b/test/Ocelot.UnitTests/RouterMiddlwareTests.cs deleted file mode 100644 index 42fca0cb..00000000 --- a/test/Ocelot.UnitTests/RouterMiddlwareTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Ocelot.UnitTests -{ - // This project can output the Class library as a NuGet Package. - // To enable this option, right-click on the project and select the Properties menu item. In the Build tab select "Produce outputs on build". - public class RouterMiddlwareTests - { - public RouterMiddlwareTests() - { - } - } -} diff --git a/test/Ocelot.UnitTests/project.json b/test/Ocelot.UnitTests/project.json index ae4974e0..ab3e6cb1 100644 --- a/test/Ocelot.UnitTests/project.json +++ b/test/Ocelot.UnitTests/project.json @@ -1,21 +1,40 @@ { - "version": "1.0.0-*", + "version": "0.0.0-dev", + + "testRunner": "xunit", "dependencies": { - "NETStandard.Library": "1.5.0-rc2-24027", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Routing": "1.0.0-rc2-final", - "Microsoft.Extensions.Logging.Console": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-rc2-final" + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.1.0", + "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", + "Microsoft.Extensions.Configuration.Json": "1.1.0", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.Extensions.Logging.Console": "1.1.0", + "Microsoft.Extensions.Logging.Debug": "1.1.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", + "Microsoft.AspNetCore.Http": "1.1.0", + "Ocelot": "0.0.0-dev", + "xunit": "2.2.0-beta2-build3300", + "dotnet-test-xunit": "2.2.0-preview2-build1029", + "Moq": "4.6.38-alpha", + "Microsoft.AspNetCore.TestHost": "1.1.0", + "Microsoft.AspNetCore.Mvc": "1.1.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "Shouldly": "2.8.2", + "TestStack.BDDfy": "4.3.2", + "Microsoft.AspNetCore.Authentication.OAuth": "1.1.0", + "Microsoft.DotNet.InternalAbstractions": "1.0.0" + }, + "runtimes": { + "win10-x64": {}, + "osx.10.11-x64":{}, + "win7-x64": {} }, - "frameworks": { - "netstandard1.5": { - "imports": "dnxcore50" + "netcoreapp1.4": { + "imports": [ + ] } - }, - - "tooling": { - "defaultNamespace": "Ocelot.UnitTests" } } diff --git a/version.ps1 b/version.ps1 new file mode 100644 index 00000000..621201b6 --- /dev/null +++ b/version.ps1 @@ -0,0 +1 @@ +.\tools\GitVersion.CommandLine\tools\GitVersion.exe \ No newline at end of file