diff --git a/.gitignore b/.gitignore index 836e0bfe..476938f0 100644 --- a/.gitignore +++ b/.gitignore @@ -243,7 +243,7 @@ tools/ .DS_Store # Ocelot acceptance test config -test/Ocelot.AcceptanceTests/configuration.json +test/Ocelot.AcceptanceTests/ocelot.json # Read the docstates _build/ @@ -251,4 +251,4 @@ _static/ _templates/ # JetBrains Rider -.idea/ \ No newline at end of file +.idea/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..ef0701aa --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +language: csharp +os: + - osx + - linux + +# Ubuntu 14.04 +sudo: required +dist: trusty + +# OS X 10.12 +osx_image: xcode9.2 + +mono: + - 4.4.2 + +dotnet: 2.1.4 + +before_install: + - git fetch --unshallow # Travis always does a shallow clone, but GitVersion needs the full history including branches and tags + - git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" + - git fetch origin + +script: + - ./build.sh + +cache: + directories: + - .packages + - tools/Addins + - tools/gitreleasemanager + - tools/GitVersion.CommandLine \ No newline at end of file diff --git a/README.md b/README.md index 6bbc77e0..edd28eaf 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ [](http://threemammals.com/ocelot) -[![Build status](https://ci.appveyor.com/api/projects/status/r6sv51qx36sis1je?svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb) +[![Build status](https://ci.appveyor.com/api/projects/status/r6sv51qx36sis1je?branch=develop&svg=true)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb) Windows (AppVeyor) +[![Build Status](https://travis-ci.org/ThreeMammals/Ocelot.svg?branch=develop)](https://travis-ci.org/ThreeMammals/Ocelot) Linux & OSX (Travis) [![Windows Build history](https://buildstats.info/appveyor/chart/TomPallister/ocelot-fcfpb?branch=develop&includeBuildsFromPullRequest=false)](https://ci.appveyor.com/project/TomPallister/ocelot-fcfpb/history?branch=develop) -[![Coverage Status](https://coveralls.io/repos/github/TomPallister/Ocelot/badge.svg?branch=develop)](https://coveralls.io/github/TomPallister/Ocelot?branch=develop) +[![Coverage Status](https://coveralls.io/repos/github/ThreeMammals/Ocelot/badge.svg?branch=develop)](https://coveralls.io/github/ThreeMammals/Ocelot?branch=develop) # Ocelot @@ -38,8 +39,9 @@ A quick list of Ocelot's capabilities for more information see the [documentatio * Routing * Request Aggregation -* Service Discovery with Consul +* Service Discovery with Consul & Eureka * Service Fabric +* WebSockets * Authentication * Authorisation * Rate Limiting @@ -50,6 +52,7 @@ A quick list of Ocelot's capabilities for more information see the [documentatio * Headers / Query String / Claims Transformation * Custom Middleware / Delegating Handlers * Configuration / Administration REST API +* Platform / Cloud agnostic ## How to install diff --git a/build.cake b/build.cake index af2f9364..f757228c 100644 --- a/build.cake +++ b/build.cake @@ -17,7 +17,7 @@ var artifactsDir = Directory("artifacts"); // unit testing var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests"); var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj"; -var minCodeCoverage = 76.4d; +var minCodeCoverage = 82d; var coverallsRepoToken = "coveralls-repo-token-ocelot"; var coverallsRepo = "https://coveralls.io/github/TomPallister/Ocelot"; @@ -189,6 +189,24 @@ Task("RunAcceptanceTests") .IsDependentOn("Compile") .Does(() => { + if(TravisCI.IsRunningOnTravisCI) + { + Information( + @"Job: + JobId: {0} + JobNumber: {1} + OSName: {2}", + BuildSystem.TravisCI.Environment.Job.JobId, + BuildSystem.TravisCI.Environment.Job.JobNumber, + BuildSystem.TravisCI.Environment.Job.OSName + ); + + if(TravisCI.Environment.Job.OSName.ToLower() == "osx") + { + return; + } + } + var settings = new DotNetCoreTestSettings { Configuration = compileConfig, @@ -205,6 +223,24 @@ Task("RunIntegrationTests") .IsDependentOn("Compile") .Does(() => { + if(TravisCI.IsRunningOnTravisCI) + { + Information( + @"Job: + JobId: {0} + JobNumber: {1} + OSName: {2}", + BuildSystem.TravisCI.Environment.Job.JobId, + BuildSystem.TravisCI.Environment.Job.JobNumber, + BuildSystem.TravisCI.Environment.Job.OSName + ); + + if(TravisCI.Environment.Job.OSName.ToLower() == "osx") + { + return; + } + } + var settings = new DotNetCoreTestSettings { Configuration = compileConfig, diff --git a/build.sh b/build.sh index 04731adf..5b3d6020 100755 --- a/build.sh +++ b/build.sh @@ -98,4 +98,4 @@ if $SHOW_VERSION; then exec mono "$CAKE_EXE" -version else exec mono "$CAKE_EXE" $SCRIPT -verbosity=$VERBOSITY -configuration=$CONFIGURATION -target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}" -fi \ No newline at end of file +fi diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 93cae138..4e11e7ed 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -1,7 +1,7 @@ Configuration ============ -An example configuration can be found `here `_. +An example configuration can be found `here `_. 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 @@ -64,20 +64,12 @@ Here is an example ReRoute configuration, You don't need to set all of these thi "UseCookieContainer": true, "UseTracing": true }, - "UseServiceDiscovery": false + "UseServiceDiscovery": false, + "DangerousAcceptAnyServerCertificateValidator": false } More information on how to use these options is below.. -Follow Redirects / Use CookieContainer -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Use HttpHandlerOptions in ReRoute configuration to set up HttpHandler behavior: -- _AllowAutoRedirect_ is a value that indicates whether the request should follow redirection responses. -Set it true if the request should automatically follow redirection responses from the Downstream resource; otherwise false. The default value is true. -- _UseCookieContainer_ is a value that indicates whether the handler uses the CookieContainer property to store server cookies and uses these cookies when sending requests. -The default value is true. - Multiple environments ^^^^^^^^^^^^^^^^^^^^^ @@ -92,15 +84,40 @@ to you .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) .AddJsonFile("appsettings.json", true, true) .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("configuration.json") + .AddJsonFile("ocelot.json") .AddJsonFile($"configuration.{hostingContext.HostingEnvironment.EnvironmentName}.json") .AddEnvironmentVariables(); }) -Ocelot should now use the environment specific configuration and fall back to configuration.json if there isnt one. +Ocelot will now use the environment specific configuration and fall back to ocelot.json if there isnt one. You also need to set the corresponding environment variable which is ASPNETCORE_ENVIRONMENT. More info on this can be found in the `asp.net core docs `_. +Merging configuration files +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This feature was requested in `Issue 296 `_ and allows users to have multiple configuration files to make managing large configurations easier. + +Instead of adding the configuration directly e.g. AddJsonFile("ocelot.json") you can call AddOcelot() like below. + +.. code-block:: csharp + + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddOcelot() + .AddEnvironmentVariables(); + }) + +In this scenario Ocelot will look for any files that match the pattern (?i)ocelot.([a-zA-Z0-9]*).json and then merge these together. If you want to set the GlobalConfiguration property you must have a file called ocelot.global.json. + +The way Ocelot merges the files is basically load them, loop over them, add any ReRoutes, add any AggregateReRoutes and if the file is called ocelot.global.json add the GlobalConfiguration aswell as any ReRoutes or AggregateReRoutes. Ocelot will then save the merged configuration to a file called ocelot.json and this will be used as the source of truth while ocelot is running. + +At the moment there is no validation at this stage it only happens when Ocelot validates the final merged configuration. This is something to be aware of when you are investigating problems. I would advise always checking what is in ocelot.json if you have any problems. + Store configuration in consul ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -112,7 +129,7 @@ If you add the following when you register your services Ocelot will attempt to .AddOcelot() .AddStoreOcelotConfigurationInConsul(); -You also need to add the following to your configuration.json. This is how Ocelot +You also need to add the following to your ocelot.json. This is how Ocelot finds your Consul agent and interacts to load and store the configuration from Consul. .. code-block:: json @@ -127,4 +144,31 @@ finds your Consul agent and interacts to load and store the configuration from C I decided to create this feature after working on the raft consensus algorithm and finding out its super hard. Why not take advantage of the fact Consul already gives you this! I guess it means if you want to use Ocelot to its fullest you take on Consul as a dependency for now. -This feature has a 3 second ttl cache before making a new request to your local consul agent. \ No newline at end of file +This feature has a 3 second ttl cache before making a new request to your local consul agent. + +Follow Redirects / Use CookieContainer +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Use HttpHandlerOptions in ReRoute configuration to set up HttpHandler behavior: + +1. AllowAutoRedirect is a value that indicates whether the request should follow redirection responses. Set it true if the request should automatically +follow redirection responses from the Downstream resource; otherwise false. The default value is false. +2. UseCookieContainer is a value that indicates whether the handler uses the CookieContainer +property to store server cookies and uses these cookies when sending requests. The default value is false. Please note +that if you are using the CookieContainer Ocelot caches the HttpClient for each downstream service. This means that all requests +to that DownstreamService will share the same cookies. `Issue 274 `_ was created because a user +noticed that the cookies were being shared. I tried to think of a nice way to handle this but I think it is impossible. If you don't cache the clients +that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container you get race conditions due to inflight +requests. This would also mean that subsequent requests dont use the cookies from the previous response! All in all not a great situation. I would avoid setting +UseCookieContainer to true unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request! + +SSL Errors +^^^^^^^^^^ + +Id you want to ignore SSL warnings / errors set the following in your ReRoute config. + +.. code-block:: json + + "DangerousAcceptAnyServerCertificateValidator": false + +I don't reccomend doing this, I suggest creating your own certificate and then getting it trusted by your local / remote machine if you can. \ No newline at end of file diff --git a/docs/features/delegatinghandlers.rst b/docs/features/delegatinghandlers.rst index 80dfa16b..445a46fb 100644 --- a/docs/features/delegatinghandlers.rst +++ b/docs/features/delegatinghandlers.rst @@ -40,7 +40,7 @@ Or transient as below... .AddTransientDelegatingHandler() Both of these Add methods have a default parameter called global which is set to false. If it is false then the intent of -the DelegatingHandler is to be applied to specific ReRoutes via configuration.json (more on that later). If it is set to true +the DelegatingHandler is to be applied to specific ReRoutes via ocelot.json (more on that later). If it is set to true then it becomes a global handler and will be applied to all ReRoutes. e.g. @@ -58,7 +58,7 @@ Or transient as below... .AddTransientDelegatingHandler(true) Finally if you want ReRoute specific DelegatingHandlers or to order your specific and / or global (more on this later) DelegatingHandlers -then you must add the following json to the specific ReRoute in configuration.json. The names in the array must match the class names of your +then you must add the following json to the specific ReRoute in ocelot.json. The names in the array must match the class names of your DelegatingHandlers for Ocelot to match them together. .. code-block:: json @@ -70,8 +70,8 @@ DelegatingHandlers for Ocelot to match them together. You can have as many DelegatingHandlers as you want and they are run in the following order: -1. Any globals that are left in the order they were added to services and are not in the DelegatingHandlers array from configuration.json. -2. Any non global DelegatingHandlers plus any globals that were in the DelegatingHandlers array from configuration.json ordered as they are in the DelegatingHandlers array. +1. Any globals that are left in the order they were added to services and are not in the DelegatingHandlers array from ocelot.json. +2. Any non global DelegatingHandlers plus any globals that were in the DelegatingHandlers array from ocelot.json ordered as they are in the DelegatingHandlers array. 3. Tracing DelegatingHandler if enabled (see tracing docs). 4. QoS DelegatingHandler if enabled (see QoS docs). 5. The HttpClient sends the HttpRequestMessage. diff --git a/docs/features/graphql.rst b/docs/features/graphql.rst new file mode 100644 index 00000000..1b527314 --- /dev/null +++ b/docs/features/graphql.rst @@ -0,0 +1,15 @@ +GraphQL +======= + +OK you got me Ocelot doesn't directly support GraphQL but so many people have asked about it I wanted to show how easy it is to integrate +the `graphql-dotnet `_ library. + + +Please see the sample project `OcelotGraphQL `_. +Using a combination of the graphql-dotnet project and Ocelot's DelegatingHandler features this is pretty easy to do. +However I do not intend to integrate more closely with GraphQL at the moment. Check out the samples readme and that should give +you enough instruction on how to do this! + +Good luck and have fun :> + + diff --git a/docs/features/headerstransformation.rst b/docs/features/headerstransformation.rst index 5063c1b7..744227e2 100644 --- a/docs/features/headerstransformation.rst +++ b/docs/features/headerstransformation.rst @@ -1,10 +1,50 @@ Headers Transformation -===================== +====================== Ocelot allows the user to transform headers pre and post downstream request. At the moment Ocelot only supports find and replace. This feature was requested `GitHub #190 `_ and I decided that it was going to be useful in various ways. -Syntax -^^^^^^ +Add to Request +^^^^^^^^^^^^^^ + +This feature was requestes in `GitHub #313 `_. + +If you want to add a header to your upstream request please add the following to a ReRoute in your ocelot.json: + +.. code-block:: json + + "UpstreamHeaderTransform": { + "Uncle": "Bob" + } + +In the example above a header with the key Uncle and value Bob would be send to to the upstream service. + +Placeholders are supported too (see below). + +Add to Response +^^^^^^^^^^^^^^^ + +This feature was requested in `GitHub #280 `_. + +If you want to add a header to your downstream response please add the following to a ReRoute in ocelot.json.. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "Uncle": "Bob" + }, + +In the example above a header with the key Uncle and value Bob would be returned by Ocelot when requesting the specific ReRoute. + +If you want to return the Butterfly APM trace id then do something like the following.. + +.. code-block:: json + + "DownstreamHeaderTransform": { + "AnyKey": "{TraceId}" + }, + +Find and Replace +^^^^^^^^^^^^^^^^ In order to transform a header first we specify the header key and then the type of transform we want e.g. @@ -17,7 +57,7 @@ The key is "Test" and the value is "http://www.bbc.co.uk/, http://ocelot.com/". Pre Downstream Request ^^^^^^^^^^^^^^^^^^^^^^ -Add the following to a ReRoute in configuration.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This header will be changed before the request downstream and will be sent to the downstream server. +Add the following to a ReRoute in ocelot.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This header will be changed before the request downstream and will be sent to the downstream server. .. code-block:: json @@ -26,9 +66,9 @@ Add the following to a ReRoute in configuration.json in order to replace http:// }, Post Downstream Request -^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^ -Add the following to a ReRoute in configuration.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This transformation will take place after Ocelot has received the response from the downstream service. +Add the following to a ReRoute in ocelot.json in order to replace http://www.bbc.co.uk/ with http://ocelot.com/. This transformation will take place after Ocelot has received the response from the downstream service. .. code-block:: json @@ -43,6 +83,7 @@ Ocelot allows placeholders that can be used in header transformation. {BaseUrl} - This will use Ocelot's base url e.g. http://localhost:5000 as its value. {DownstreamBaseUrl} - This will use the downstream services base url e.g. http://localhost:5000 as its value. This only works for DownstreamHeaderTransform at the moment. +{TraceId} - This will use the Butterfly APM Trace Id. This only works for DownstreamHeaderTransform at the moment. Handling 302 Redirects ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/features/loadbalancer.rst b/docs/features/loadbalancer.rst index f587aa22..c2cc0f26 100644 --- a/docs/features/loadbalancer.rst +++ b/docs/features/loadbalancer.rst @@ -16,7 +16,7 @@ You must choose in your configuration which load balancer to use. Configuration ^^^^^^^^^^^^^ -The following shows how to set up multiple downstream services for a ReRoute using configuration.json and then select the LeadConnection load balancer. This is the simplest way to get load balancing set up. +The following shows how to set up multiple downstream services for a ReRoute using ocelot.json and then select the LeadConnection load balancer. This is the simplest way to get load balancing set up. .. code-block:: json diff --git a/docs/features/logging.rst b/docs/features/logging.rst index 313f62ff..b09a26cf 100644 --- a/docs/features/logging.rst +++ b/docs/features/logging.rst @@ -11,4 +11,10 @@ Finally if logging is set to trace level Ocelot will log starting, finishing and 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. \ No newline at end of file +to true for logging settings. Nicely onto the next feature. + +Warning +^^^^^^^ + +If you are logging to Console you will get terrible performance. I have had so many issues about performance issues with Ocelot +and it is always logging level Debug, logging to Console :) Make sure you are logging to something proper in production :) diff --git a/docs/features/middlewareinjection.rst b/docs/features/middlewareinjection.rst index f25cfa26..44f44fb3 100644 --- a/docs/features/middlewareinjection.rst +++ b/docs/features/middlewareinjection.rst @@ -9,7 +9,7 @@ and override middleware. This is done as follos. .. code-block:: csharp - var configuration = new OcelotMiddlewareConfiguration + var configuration = new OcelotPipelineConfiguration { PreErrorResponderMiddleware = async (ctx, next) => { @@ -38,4 +38,4 @@ The user can set functions against the following. * 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. \ No newline at end of file +after as Ocelot does not call the next middleware. diff --git a/docs/features/qualityofservice.rst b/docs/features/qualityofservice.rst index 9eebf923..17bf373d 100644 --- a/docs/features/qualityofservice.rst +++ b/docs/features/qualityofservice.rst @@ -17,6 +17,17 @@ Add the following section to a ReRoute configuration. You must set a number greater than 0 against ExceptionsAllowedBeforeBreaking for this rule to be implemented. Duration of break is how long the circuit breaker will stay open for after it is tripped. -TimeoutValue means ff a request takes more than 5 seconds it will automatically be timed out. +TimeoutValue means if a request takes more than 5 seconds it will automatically be timed out. -If you do not add a QoS section QoS will not be used. \ No newline at end of file +You can set the TimeoutValue in isoldation of the ExceptionsAllowedBeforeBreaking and DurationOfBreak options. + +.. code-block:: json + + "QoSOptions": { + "TimeoutValue":5000 + } + +There is no point setting the other two in isolation as they affect each other :) + +If you do not add a QoS section QoS will not be used however Ocelot will default to a 90 second timeout +on all downstream requests. If someone needs this to be configurable open an issue. \ No newline at end of file diff --git a/docs/features/ratelimiting.rst b/docs/features/ratelimiting.rst index d929c905..7f9c0768 100644 --- a/docs/features/ratelimiting.rst +++ b/docs/features/ratelimiting.rst @@ -23,7 +23,7 @@ Period - This value specifies the period, such as 1s, 5m, 1h,1d and so on. PeriodTimespan - This value specifies that we can retry after a certain number of seconds. Limit - This value specifies the maximum number of requests that a client can make in a defined period. -You can also set the following in the GlobalConfiguration part of configuration.json +You can also set the following in the GlobalConfiguration part of ocelot.json .. code-block:: json diff --git a/docs/features/requestaggregation.rst b/docs/features/requestaggregation.rst index b359eb72..a5ca30f6 100644 --- a/docs/features/requestaggregation.rst +++ b/docs/features/requestaggregation.rst @@ -5,12 +5,114 @@ Ocelot allow's you to specify Aggregate ReRoutes that compose multiple normal Re a client that is making multiple requests to a server where it could just be one. This feature allows you to start implementing back end for a front end type architecture with Ocelot. -This feature was requested as part of `Issue 79 `_ . +This feature was requested as part of `Issue 79 `_ and further improvements were made as part of `Issue 298 `_. -In order to set this up you must do something like the following in your configuration.json. Here we have specified two normal ReRoutes and each one has a Key property. +In order to set this up you must do something like the following in your ocelot.json. Here we have specified two normal ReRoutes and each one has a Key property. We then specify an Aggregate that composes the two ReRoutes using their keys in the ReRouteKeys list and says then we have the UpstreamPathTemplate which works like a normal ReRoute. Obviously you cannot have duplicate UpstreamPathTemplates between ReRoutes and Aggregates. You can use all of Ocelot's normal ReRoute options apart from RequestIdKey (explained in gotchas below). +Advanced register your own Aggregators +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Ocelot started with just the basic request aggregation and since then we have added a more advanced method that let's the user take in the responses from the +downstream services and then aggregate them into a response object. + +The ocelot.json setup is pretty much the same as the basic aggregation approach apart from you need to add an Aggregator property like below. + +.. code-block:: json + + { + "ReRoutes": [ + { + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/laura", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51881 + } + ], + "Key": "Laura" + }, + { + "DownstreamPathTemplate": "/", + "UpstreamPathTemplate": "/tom", + "UpstreamHttpMethod": [ + "Get" + ], + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 51882 + } + ], + "Key": "Tom" + } + ], + "Aggregates": [ + { + "ReRouteKeys": [ + "Tom", + "Laura" + ], + "UpstreamPathTemplate": "/", + "Aggregator": "FakeDefinedAggregator" + } + ] + } + +Here we have added an aggregator called FakeDefinedAggregator. Ocelot is going to look for this aggregator when it tries to aggregate this ReRoute. + +In order to make the aggregator available we must add the FakeDefinedAggregator to the OcelotBuilder like below. + +.. code-block:: csharp + + services + .AddOcelot() + .AddSingletonDefinedAggregator(); + +Now when Ocelot tries to aggregate the ReRoute above it will find the FakeDefinedAggregator in the container and use it to aggregate the ReRoute. +Because the FakeDefinedAggregator is registered in the container you can add any dependencies it needs into the container like below. + +.. code-block:: csharp + + services.AddSingleton(); + + services + .AddOcelot() + .AddSingletonDefinedAggregator(); + +In this example FooAggregator takes a dependency on FooDependency and it will be resolved by the container. + +In addition to this Ocelot lets you add transient aggregators like below. + +.. code-block:: csharp + + services + .AddOcelot() + .AddTransientDefinedAggregator(); + +In order to make an Aggregator you must implement this interface. + +.. code-block:: csharp + + public interface IDefinedAggregator + { + Task Aggregate(List responses); + } + +With this feature you can pretty much do whatever you want because DownstreamResponse contains Content, Headers and Status Code. We can add extra things if needed +just raise an issue on GitHub. Please note if the HttpClient throws an exception when making a request to a ReRoute in the aggregate then you will not get a DownstreamResponse for +it but you would for any that succeed. If it does throw an exception this will be logged. + +Basic expecting JSON from Downstream Services +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + .. code-block:: json { @@ -65,9 +167,6 @@ If the ReRoute /tom returned a body of {"Age": 19} and /laura returned {"Age": 2 {"Tom":{"Age": 19},"Laura":{"Age": 25}} -Gotcha's / Further info -^^^^^^^^^^^^^^^^^^^^^^^ - At the moment the aggregation is very simple. Ocelot just gets the response from your downstream service and sticks it into a json dictionary as above. With the ReRoute key being the key of the dictionary and the value the response body from your downstream service. You can see that the object is just JSON without any pretty spaces etc. @@ -76,20 +175,13 @@ All headers will be lost from the downstream services response. Ocelot will always return content type application/json with an aggregate request. +If you downstream services return a 404 the aggregate will just return nothing for that downstream service. +It will not change the aggregate response into a 404 even if all the downstreams return a 404. + +Gotcha's / Further info +----------------------- + You cannot use ReRoutes with specific RequestIdKeys as this would be crazy complicated to track. Aggregation only supports the GET HTTP Verb. -If you downstream services return a 404 the aggregate will just return nothing for that downstream service. -It will not change the aggregate response into a 404 even if all the downstreams return a 404. - -Future -^^^^^^ - -There are loads of cool ways to enchance this such as.. - -What happens when downstream goes slow..should we timeout? -Can we do something like GraphQL where the user chooses what fields are returned? -Can we handle 404 better etc? -Can we make this not just support a JSON dictionary response? - diff --git a/docs/features/requestid.rst b/docs/features/requestid.rst index 6e4f239b..37752eda 100644 --- a/docs/features/requestid.rst +++ b/docs/features/requestid.rst @@ -12,7 +12,7 @@ In order to use the reques tid feature you have two options. *Global* -In your configuration.json set the following in the GlobalConfiguration section. This will be used for all requests into Ocelot. +In your ocelot.json set the following in the GlobalConfiguration section. This will be used for all requests into Ocelot. .. code-block:: json @@ -24,7 +24,7 @@ I reccomend using the GlobalConfiguration unless you really need it to be ReRout *ReRoute* -If you want to override this for a specific ReRoute add the following to configuration.json for the specific ReRoute. +If you want to override this for a specific ReRoute add the following to ocelot.json for the specific ReRoute. .. code-block:: json diff --git a/docs/features/routing.rst b/docs/features/routing.rst index 4215aa44..664021a6 100644 --- a/docs/features/routing.rst +++ b/docs/features/routing.rst @@ -139,4 +139,42 @@ The ReRoute above will only be matched when the host header value is somedomain. If you do not set UpstreamHost on a ReRoue then any host header can match it. This is basically a catch all and preservers existing functionality at the time of building the feature. This means that if you have two ReRoutes that are the same apart from the UpstreamHost where one is null and the other set. Ocelot will favour the one that has been set. -This feature was requested as part of `Issue 216 `_ . \ No newline at end of file +This feature was requested as part of `Issue 216 `_ . + +Priority +^^^^^^^^ + +In `Issue 270 `_ I finally decided to expose the ReRoute priority in +ocelot.json. This means you can decide in what order you want your ReRoutes to match the Upstream HttpRequest. + +In order to get this working add the following to a ReRoute in ocelot.json, 0 is just an example value here but will explain below. + +.. code-block:: json + + { + "Priority": 0 + } + +0 is the lowest priority, Ocelot will always use 0 for /{catchAll} ReRoutes and this is still hardcoded. After that you are free +to set any priority you wish. + +e.g. you could have + +.. code-block:: json + + { + "UpstreamPathTemplate": "/goods/{catchAll}" + "Priority": 0 + } + +and + +.. code-block:: json + + { + "UpstreamPathTemplate": "/goods/delete" + "Priority": 1 + } + +In the example above if you make a request into Ocelot on /goods/delete Ocelot will match /goods/delete ReRoute. Previously it would have +matched /goods/{catchAll} (because this is the first ReRoute in the list!). \ No newline at end of file diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index f8cadd2e..6fe25c12 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -38,3 +38,53 @@ and LeastConnection algorithm you can use. If no load balancer is specified Ocel } When this is set up Ocelot will lookup the downstream host and port from the service discover provider and load balance requests across any available services. + +ACL Token +--------- + +If you are using ACL with Consul Ocelot supports adding the X-Consul-Token header. In order so this to work you must add the additional property below. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Host": "localhost", + "Port": 9500, + "Token": "footoken" + } + +Ocelot will add this token to the consul client that it uses to make requests and that is then used for every request. + +Eureka +^^^^^^ + +This feature was requested as part of `Issue 262 `_ . to add support for Netflix's +Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe `_ which is something +to do with `Pivotal `_! Anyway enough of the background. + +In order to get this working add the following to ocelot.json.. + +.. code-block:: json + + "ServiceDiscoveryProvider": { + "Type": "Eureka" + } + +And following the guide `Here `_ you may also need to add some stuff to appsettings.json. For example the json below tells the steeltoe / pivotal services where to look for the service discovery server and if the service should register with it. + +.. code-block:: json + + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": false, + "shouldFetchRegistry": true + } + } + +I am told that if shouldRegisterWithEureka is false then shouldFetchRegistry will defaut to true so you don't need it explicitly but left it in there. + +Ocelot will now register all the necessary services when it starts up and if you have the json above will register itself with +Eureka. One of the services polls Eureka every 30 seconds (default) and gets the latest service state and persists this in memory. +When Ocelot asks for a given service it is retrieved from memory so performance is not a big problem. Please note that this code +is provided by the Pivotal.Discovery.Client NuGet package so big thanks to them for all the hard work. + diff --git a/docs/features/tracing.rst b/docs/features/tracing.rst index 0a896d45..aa4b1fd3 100644 --- a/docs/features/tracing.rst +++ b/docs/features/tracing.rst @@ -20,7 +20,7 @@ In your ConfigureServices method option.Service = "Ocelot"; }); -Then in your configuration.json add the following to the ReRoute you want to trace.. +Then in your ocelot.json add the following to the ReRoute you want to trace.. .. code-block:: json diff --git a/docs/features/websockets.rst b/docs/features/websockets.rst new file mode 100644 index 00000000..624f42a9 --- /dev/null +++ b/docs/features/websockets.rst @@ -0,0 +1,68 @@ +Websockets +========== + +Ocelot supports proxying websockets with some extra bits. This functionality was requested in `Issue 212 `_. + +In order to get websocket proxying working with Ocelot you need to do the following. + +In your Configure method you need to tell your application to use WebSockets. + +.. code-block:: csharp + + Configure(app => + { + app.UseWebSockets(); + app.UseOcelot().Wait(); + }) + +Then in your ocelot.json add the following to proxy a ReRoute using websockets. + +.. code-block:: json + + { + "DownstreamPathTemplate": "/ws", + "UpstreamPathTemplate": "/", + "DownstreamScheme": "ws", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5001 + } + ], + } + +With this configuration set Ocelot will match any websocket traffic that comes in on / and proxy it to localhost:5001/ws. To make this clearer +Ocelot will receive messages from the upstream client, proxy these to the downstream service, receive messages from the downstream service and +proxy these to the upstream client. + +Supported +^^^^^^^^^ + +1. Load Balancer +2. Routing +3. Service Discovery + +This means that you can set up your downstream services running websockets and either have multiple DownstreamHostAndPorts in your ReRoute +config or hook your ReRoute into a service discovery provider and then load balance requests...Which I think is pretty cool :) + +Not Supported +^^^^^^^^^^^^^ + +Unfortunately a lot of Ocelot's features are non websocket specific such as header and http client stuff. I've listed what won't work below. + +1. Tracing +2. RequestId +3. Request Aggregation +4. Rate Limiting +5. Quality of Service +6. Middleware Injection +7. Header Transformation +8. Delegating Handlers +9. Claims Transformation +10. Caching +11. Authentication - If anyone requests it we might be able to do something with basic authentication. +12. Authorisation + +I'm not 100% sure what will happen with this feature when it get's into the wild so please make sure you test thoroughly! + + diff --git a/docs/index.rst b/docs/index.rst index 43a6c436..7989fcdd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,10 +21,12 @@ Thanks for taking a look at the Ocelot documentation. Please use the left hand n features/configuration features/routing features/requestaggregation + features/graphql features/servicediscovery features/servicefabric features/authentication features/authorisation + features/websockets features/administration features/ratelimiting features/caching diff --git a/docs/introduction/bigpicture.rst b/docs/introduction/bigpicture.rst index 989c0f5c..b047abb4 100644 --- a/docs/introduction/bigpicture.rst +++ b/docs/introduction/bigpicture.rst @@ -1,7 +1,7 @@ Big Picture =========== -Ocleot is aimed at people using .NET running +Ocelot is aimed at people using .NET running a micro services / service orientated architecture that need a unified point of entry into their system. diff --git a/docs/introduction/gettingstarted.rst b/docs/introduction/gettingstarted.rst index 8f635fdc..089acc4f 100644 --- a/docs/introduction/gettingstarted.rst +++ b/docs/introduction/gettingstarted.rst @@ -9,7 +9,7 @@ built to netcoreapp2.0 `this `_. **Configuration** -The following is a very basic configuration.json. It won't do anything but should get Ocelot starting. +The following is a very basic ocelot.json. It won't do anything but should get Ocelot starting. .. code-block:: json @@ -55,7 +55,7 @@ AddOcelot() (adds ocelot services), UseOcelot().Wait() (sets up all the Ocelot m .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) .AddJsonFile("appsettings.json", true, true) .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("configuration.json") + .AddJsonFile("ocelot.json") .AddEnvironmentVariables(); }) .ConfigureServices(s => { @@ -87,7 +87,7 @@ All versions can be found `here `_. **Configuration** -The following is a very basic configuration.json. It won't do anything but should get Ocelot starting. +The following is a very basic ocelot.json. It won't do anything but should get Ocelot starting. .. code-block:: json @@ -135,7 +135,7 @@ An example startup using a json file for configuration can be seen below. .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddJsonFile("configuration.json") + .AddJsonFile("ocelot.json") .AddEnvironmentVariables(); Configuration = builder.Build(); @@ -154,4 +154,4 @@ An example startup using a json file for configuration can be seen below. } } -This is pretty much all you need to get going. \ No newline at end of file +This is pretty much all you need to get going. diff --git a/docs/introduction/notsupported.rst b/docs/introduction/notsupported.rst index 35c916a0..e578bca2 100644 --- a/docs/introduction/notsupported.rst +++ b/docs/introduction/notsupported.rst @@ -7,7 +7,10 @@ Ocelot does not support... * 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 :( -* Swagger - I have looked multiple times at building swagger.json out of the Ocelot configuration.json but it doesnt fit into the vision I have for Ocelot. If you would like to have Swagger in Ocelot then you must roll your own swagger.json and do the following in your Startup.cs or Program.cs. The code sample below registers a piece of middleware that loads your hand rolled swagger.json and returns it on /swagger/v1/swagger.json. It then registers the SwaggerUI middleware from Swashbuckle.AspNetCore +* Swagger - I have looked multiple times at building swagger.json out of the Ocelot ocelot.json but it doesnt fit into the vision +I have for Ocelot. If you would like to have Swagger in Ocelot then you must roll your own swagger.json and do the following in your +Startup.cs or Program.cs. The code sample below registers a piece of middleware that loads your hand rolled swagger.json and returns +it on /swagger/v1/swagger.json. It then registers the SwaggerUI middleware from Swashbuckle.AspNetCore .. code-block:: csharp @@ -25,8 +28,16 @@ Ocelot does not support... app.UseOcelot().Wait(); -The main reasons why I don't think Swagger makes sense is we already hand roll our definition in configuration.json. If we want people developing against Ocelot to be able to see what routes are available then either share the configuration.json with them (This should be as easy as granting access to a repo etc) or use the Ocelot administration API so that they can query Ocelot for the configuration. +The main reasons why I don't think Swagger makes sense is we already hand roll our definition in ocelot.json. +If we want people developing against Ocelot to be able to see what routes are available then either share the ocelot.json +with them (This should be as easy as granting access to a repo etc) or use the Ocelot administration API so that they can query Ocelot for the configuration. -In addition to this many people will configure Ocelot to proxy all traffic like /products/{everything} to there product service and you would not be describing what is actually available if you parsed this and turned it into a Swagger path. Also Ocelot has no concept of the models that the downstream services can return and linking to the above problem the same endpoint can return multiple models. Ocelot does not know what models might be used in POST, PUT etc so it all gets a bit messy and finally the Swashbuckle package doesnt reload swagger.json if it changes during runtime. Ocelot's configuration can change during runtime so the Swagger and Ocelot information would not match. Unless I rolled my own Swagger implementation. +In addition to this many people will configure Ocelot to proxy all traffic like /products/{everything} to there product service +and you would not be describing what is actually available if you parsed this and turned it into a Swagger path. Also Ocelot has +no concept of the models that the downstream services can return and linking to the above problem the same endpoint can return +multiple models. Ocelot does not know what models might be used in POST, PUT etc so it all gets a bit messy and finally the Swashbuckle +package doesnt reload swagger.json if it changes during runtime. Ocelot's configuration can change during runtime so the Swagger and Ocelot +information would not match. Unless I rolled my own Swagger implementation. -If the user wants something to easily test against the Ocelot API then I suggest using Postman as a simple way to do this. It might even be possible to write something that maps configuration.json to the postman json spec. However I don't intend to do this. \ No newline at end of file +If the user wants something to easily test against the Ocelot API then I suggest using Postman as a simple way to do this. It might +even be possible to write something that maps ocelot.json to the postman json spec. However I don't intend to do this. \ No newline at end of file diff --git a/samples/OcelotGraphQL/OcelotGraphQL.csproj b/samples/OcelotGraphQL/OcelotGraphQL.csproj new file mode 100644 index 00000000..ec015d1b --- /dev/null +++ b/samples/OcelotGraphQL/OcelotGraphQL.csproj @@ -0,0 +1,18 @@ + + + netcoreapp2.0 + + + + PreserveNewest + + + + + + + + + + + \ No newline at end of file diff --git a/samples/OcelotGraphQL/Program.cs b/samples/OcelotGraphQL/Program.cs new file mode 100644 index 00000000..81938f9c --- /dev/null +++ b/samples/OcelotGraphQL/Program.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Ocelot.Middleware; +using Ocelot.DependencyInjection; +using GraphQL.Types; +using GraphQL; +using Ocelot.Requester; +using Ocelot.Responses; +using System.Net.Http; +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using System.Threading; + +namespace OcelotGraphQL +{ + public class Hero + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class Query + { + private List _heroes = new List + { + new Hero { Id = 1, Name = "R2-D2" }, + new Hero { Id = 2, Name = "Batman" }, + new Hero { Id = 3, Name = "Wonder Woman" }, + new Hero { Id = 4, Name = "Tom Pallister" } + }; + + [GraphQLMetadata("hero")] + public Hero GetHero(int id) + { + return _heroes.FirstOrDefault(x => x.Id == id); + } + } + + public class GraphQlDelegatingHandler : DelegatingHandler + { + private readonly ISchema _schema; + + public GraphQlDelegatingHandler(ISchema schema) + { + _schema = schema; + } + + protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + //try get query from body, could check http method :) + var query = await request.Content.ReadAsStringAsync(); + + //if not body try query string, dont hack like this in real world.. + if(query.Length == 0) + { + var decoded = WebUtility.UrlDecode(request.RequestUri.Query); + query = decoded.Replace("?query=", ""); + } + + var result = _schema.Execute(_ => + { + _.Query = query; + }); + + //maybe check for errors and headers etc in real world? + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(result) + }; + + //ocelot will treat this like any other http request... + return response; + } + } + + public class Program + { + public static void Main(string[] args) + { + var schema = Schema.For(@" + type Hero { + id: Int + name: String + } + + type Query { + hero(id: Int): Hero + } + ", _ => { + _.Types.Include(); + }); + + new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json") + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => { + s.AddSingleton(schema); + s.AddOcelot() + .AddSingletonDelegatingHandler(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .UseIISIntegration() + .Configure(app => + { + app.UseOcelot().Wait(); + }) + .Build() + .Run(); + } + } +} diff --git a/samples/OcelotGraphQL/README.md b/samples/OcelotGraphQL/README.md new file mode 100644 index 00000000..b2101d56 --- /dev/null +++ b/samples/OcelotGraphQL/README.md @@ -0,0 +1,71 @@ +# Ocelot using GraphQL example + +Loads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. +I would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorisation / authentication or I would +bring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. + +## Example + +If you run this project with + +$ dotnet run + +Use postman or something to make the following requests and you can see Ocelot and GraphQL in action together... + +GET http://localhost:5000/graphql?query={ hero(id: 4) { id name } } + +RESPONSE +```json + { + "data": { + "hero": { + "id": 4, + "name": "Tom Pallister" + } + } + } +``` + +POST http://localhost:5000/graphql + +BODY +```json + { hero(id: 4) { id name } } +``` + +RESPONSE +```json + { + "data": { + "hero": { + "id": 4, + "name": "Tom Pallister" + } + } + } +``` + +## Notes + +Please note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g. + +```json +{ + "ReRoutes": [ + { + "DownstreamPathTemplate": "/graphql", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "yourgraphqlhost.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/graphql", + "DelegatingHandlers": [ + "GraphQlDelegatingHandler" + ] + } + ] + } +``` \ No newline at end of file diff --git a/samples/OcelotGraphQL/ocelot.json b/samples/OcelotGraphQL/ocelot.json new file mode 100644 index 00000000..115006f9 --- /dev/null +++ b/samples/OcelotGraphQL/ocelot.json @@ -0,0 +1,19 @@ +{ + "ReRoutes": [ + { + "DownstreamPathTemplate": "/", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 80 + } + ], + "UpstreamPathTemplate": "/graphql", + "DelegatingHandlers": [ + "GraphQlDelegatingHandler" + ] + } + ] + } + \ No newline at end of file diff --git a/samples/OcelotServiceFabric/.gitignore b/samples/OcelotServiceFabric/.gitignore index 733dbb07..84aa4b08 100644 --- a/samples/OcelotServiceFabric/.gitignore +++ b/samples/OcelotServiceFabric/.gitignore @@ -13,7 +13,7 @@ # Service fabric OcelotApplicationApiGatewayPkg/Code OcelotApplication/OcelotApplicationApiGatewayPkg/Code/appsettings.json -OcelotApplication/OcelotApplicationApiGatewayPkg/Code/configuration.json +OcelotApplication/OcelotApplicationApiGatewayPkg/Code/ocelot.json OcelotApplication/OcelotApplicationApiGatewayPkg/Code/runtimes/ OcelotApplicationServicePkg/Code OcelotApplication/OcelotApplicationApiGatewayPkg/Code/web.config diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj index 1ff69c09..08324cbe 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj +++ b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/OcelotApplicationApiGateway.csproj @@ -8,7 +8,7 @@ OcelotApplicationApiGateway - + PreserveNewest diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs index 6685e11f..7d913cfa 100644 --- a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs +++ b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/WebCommunicationListener.cs @@ -67,7 +67,7 @@ namespace OcelotApplicationApiGateway .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) .AddJsonFile("appsettings.json", true, true) .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("configuration.json") + .AddJsonFile("ocelot.json") .AddEnvironmentVariables(); }) .ConfigureLogging((hostingContext, logging) => diff --git a/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/configuration.json b/samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json similarity index 100% rename from samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/configuration.json rename to samples/OcelotServiceFabric/src/OcelotApplicationApiGateway/ocelot.json diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs index bd4683d0..2c70e394 100644 --- a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs @@ -12,20 +12,19 @@ namespace Ocelot.Authentication.Middleware public class AuthenticationMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; - private readonly IOcelotLogger _logger; public AuthenticationMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory) + : base(loggerFactory.CreateLogger()) { _next = next; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) { if (IsAuthenticatedRoute(context.DownstreamReRoute)) { - _logger.LogDebug($"{context.HttpContext.Request.Path} is an authenticated route. {MiddlewareName} checking if client is authenticated"); + Logger.LogInformation($"{context.HttpContext.Request.Path} is an authenticated route. {MiddlewareName} checking if client is authenticated"); var result = await context.HttpContext.AuthenticateAsync(context.DownstreamReRoute.AuthenticationOptions.AuthenticationProviderKey); @@ -33,25 +32,22 @@ namespace Ocelot.Authentication.Middleware if (context.HttpContext.User.Identity.IsAuthenticated) { - _logger.LogDebug($"Client has been authenticated for {context.HttpContext.Request.Path}"); + Logger.LogInformation($"Client has been authenticated for {context.HttpContext.Request.Path}"); await _next.Invoke(context); } else { - var error = new List - { - new UnauthenticatedError( - $"Request for authenticated route {context.HttpContext.Request.Path} by {context.HttpContext.User.Identity.Name} was unauthenticated") - }; + var error = new UnauthenticatedError( + $"Request for authenticated route {context.HttpContext.Request.Path} by {context.HttpContext.User.Identity.Name} was unauthenticated"); - _logger.LogError($"Client has NOT been authenticated for {context.HttpContext.Request.Path} and pipeline error set. {error.ToErrorString()}"); + Logger.LogWarning($"Client has NOT been authenticated for {context.HttpContext.Request.Path} and pipeline error set. {error}"); SetPipelineError(context, error); } } else { - _logger.LogTrace($"No authentication needed for {context.HttpContext.Request.Path}"); + Logger.LogInformation($"No authentication needed for {context.HttpContext.Request.Path}"); await _next.Invoke(context); } diff --git a/src/Ocelot/Authorisation/ClaimsAuthoriser.cs b/src/Ocelot/Authorisation/ClaimsAuthoriser.cs index 3ca7809e..d67c4d5f 100644 --- a/src/Ocelot/Authorisation/ClaimsAuthoriser.cs +++ b/src/Ocelot/Authorisation/ClaimsAuthoriser.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Security.Claims; -using Ocelot.Errors; using Ocelot.Responses; namespace Ocelot.Authorisation @@ -32,19 +31,13 @@ namespace Ocelot.Authorisation var authorised = values.Data.Contains(required.Value); if (!authorised) { - return new ErrorResponse(new List - { - new ClaimValueNotAuthorisedError( - $"claim value: {values.Data} is not the same as required value: {required.Value} for type: {required.Key}") - }); + return new ErrorResponse(new ClaimValueNotAuthorisedError( + $"claim value: {values.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 ErrorResponse(new UserDoesNotHaveClaimError($"user does not have claim {required.Key}")); } } diff --git a/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs b/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs index 4ace7dbc..82fc3af7 100644 --- a/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs +++ b/src/Ocelot/Authorisation/Middleware/AuthorisationMiddleware.cs @@ -13,30 +13,29 @@ private readonly OcelotRequestDelegate _next; private readonly IClaimsAuthoriser _claimsAuthoriser; private readonly IScopesAuthoriser _scopesAuthoriser; - private readonly IOcelotLogger _logger; public AuthorisationMiddleware(OcelotRequestDelegate next, IClaimsAuthoriser claimsAuthoriser, IScopesAuthoriser scopesAuthoriser, IOcelotLoggerFactory loggerFactory) + :base(loggerFactory.CreateLogger()) { _next = next; _claimsAuthoriser = claimsAuthoriser; _scopesAuthoriser = scopesAuthoriser; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) { if (IsAuthenticatedRoute(context.DownstreamReRoute)) { - _logger.LogDebug("route is authenticated scopes must be checked"); + Logger.LogInformation("route is authenticated scopes must be checked"); var authorised = _scopesAuthoriser.Authorise(context.HttpContext.User, context.DownstreamReRoute.AuthenticationOptions.AllowedScopes); if (authorised.IsError) { - _logger.LogDebug("error authorising user scopes"); + Logger.LogWarning("error authorising user scopes"); SetPipelineError(context, authorised.Errors); return; @@ -44,29 +43,26 @@ if (IsAuthorised(authorised)) { - _logger.LogDebug("user scopes is authorised calling next authorisation checks"); + Logger.LogInformation("user scopes is authorised calling next authorisation checks"); } else { - _logger.LogDebug("user scopes is not authorised setting pipeline error"); + Logger.LogWarning("user scopes is not authorised setting pipeline error"); - SetPipelineError(context, new List - { - new UnauthorisedError( - $"{context.HttpContext.User.Identity.Name} unable to access {context.DownstreamReRoute.UpstreamPathTemplate.Value}") - }); + SetPipelineError(context, new UnauthorisedError( + $"{context.HttpContext.User.Identity.Name} unable to access {context.DownstreamReRoute.UpstreamPathTemplate.Value}")); } } if (IsAuthorisedRoute(context.DownstreamReRoute)) { - _logger.LogDebug("route is authorised"); + Logger.LogInformation("route is authorised"); var authorised = _claimsAuthoriser.Authorise(context.HttpContext.User, context.DownstreamReRoute.RouteClaimsRequirement); if (authorised.IsError) { - _logger.LogDebug($"Error whilst authorising {context.HttpContext.User.Identity.Name} for {context.HttpContext.User.Identity.Name}. Setting pipeline error"); + Logger.LogWarning($"Error whilst authorising {context.HttpContext.User.Identity.Name}. Setting pipeline error"); SetPipelineError(context, authorised.Errors); return; @@ -74,22 +70,19 @@ if (IsAuthorised(authorised)) { - _logger.LogDebug($"{context.HttpContext.User.Identity.Name} has succesfully been authorised for {context.DownstreamReRoute.UpstreamPathTemplate.Value}. Calling next middleware"); + Logger.LogInformation($"{context.HttpContext.User.Identity.Name} has succesfully been authorised for {context.DownstreamReRoute.UpstreamPathTemplate.Value}."); await _next.Invoke(context); } else { - _logger.LogDebug($"{context.HttpContext.User.Identity.Name} is not authorised to access {context.DownstreamReRoute.UpstreamPathTemplate.Value}. Setting pipeline error"); + Logger.LogWarning($"{context.HttpContext.User.Identity.Name} is not authorised to access {context.DownstreamReRoute.UpstreamPathTemplate.Value}. Setting pipeline error"); - SetPipelineError(context, new List - { - new UnauthorisedError($"{context.HttpContext.User.Identity.Name} is not authorised to access {context.DownstreamReRoute.UpstreamPathTemplate.Value}") - }); + SetPipelineError(context, new UnauthorisedError($"{context.HttpContext.User.Identity.Name} is not authorised to access {context.DownstreamReRoute.UpstreamPathTemplate.Value}")); } } else { - _logger.LogDebug($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} route does not require user to be authorised"); + Logger.LogInformation($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} route does not require user to be authorised"); await _next.Invoke(context); } } diff --git a/src/Ocelot/Authorisation/ScopesAuthoriser.cs b/src/Ocelot/Authorisation/ScopesAuthoriser.cs index 1654e2a1..4b999c10 100644 --- a/src/Ocelot/Authorisation/ScopesAuthoriser.cs +++ b/src/Ocelot/Authorisation/ScopesAuthoriser.cs @@ -1,5 +1,4 @@ using IdentityModel; -using Ocelot.Errors; using Ocelot.Responses; using System.Collections.Generic; using System.Security.Claims; @@ -38,11 +37,8 @@ namespace Ocelot.Authorisation if (matchesScopes.Count == 0) { - return new ErrorResponse(new List - { - new ScopeNotAuthorisedError( - $"no one user scope: '{string.Join(",", userScopes)}' match with some allowed scope: '{string.Join(",", routeAllowedScopes)}'") - }); + return new ErrorResponse( + new ScopeNotAuthorisedError($"no one user scope: '{string.Join(",", userScopes)}' match with some allowed scope: '{string.Join(",", routeAllowedScopes)}'")); } return new OkResponse(true); diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs index 3d128b72..aee3bec3 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -2,19 +2,16 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using System.IO; -using Ocelot.DownstreamRouteFinder.Middleware; +using Ocelot.Middleware.Multiplexer; namespace Ocelot.Cache.Middleware { public class OutputCacheMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; - private readonly IOcelotLogger _logger; private readonly IOcelotCache _outputCache; private readonly IRegionCreator _regionCreator; @@ -22,10 +19,10 @@ namespace Ocelot.Cache.Middleware IOcelotLoggerFactory loggerFactory, IOcelotCache outputCache, IRegionCreator regionCreator) + :base(loggerFactory.CreateLogger()) { _next = next; _outputCache = outputCache; - _logger = loggerFactory.CreateLogger(); _regionCreator = regionCreator; } @@ -37,31 +34,31 @@ namespace Ocelot.Cache.Middleware return; } - var downstreamUrlKey = $"{context.DownstreamRequest.Method.Method}-{context.DownstreamRequest.RequestUri.OriginalString}"; + var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"; - _logger.LogDebug("started checking cache for {downstreamUrlKey}", downstreamUrlKey); + Logger.LogDebug($"Started checking cache for {downstreamUrlKey}"); var cached = _outputCache.Get(downstreamUrlKey, context.DownstreamReRoute.CacheOptions.Region); if (cached != null) { - _logger.LogDebug("cache entry exists for {downstreamUrlKey}", downstreamUrlKey); + Logger.LogDebug($"cache entry exists for {downstreamUrlKey}"); var response = CreateHttpResponseMessage(cached); SetHttpResponseMessageThisRequest(context, response); - _logger.LogDebug("finished returned cached response for {downstreamUrlKey}", downstreamUrlKey); + Logger.LogDebug($"finished returned cached response for {downstreamUrlKey}"); return; } - _logger.LogDebug("no resonse cached for {downstreamUrlKey}", downstreamUrlKey); + Logger.LogDebug($"no resonse cached for {downstreamUrlKey}"); await _next.Invoke(context); if (context.IsError) { - _logger.LogDebug("there was a pipeline error for {downstreamUrlKey}", downstreamUrlKey); + Logger.LogDebug($"there was a pipeline error for {downstreamUrlKey}"); return; } @@ -70,41 +67,34 @@ namespace Ocelot.Cache.Middleware _outputCache.Add(downstreamUrlKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region); - _logger.LogDebug("finished response added to cache for {downstreamUrlKey}", downstreamUrlKey); + Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}"); } - private void SetHttpResponseMessageThisRequest(DownstreamContext context, HttpResponseMessage response) + private void SetHttpResponseMessageThisRequest(DownstreamContext context, DownstreamResponse response) { context.DownstreamResponse = response; } - internal HttpResponseMessage CreateHttpResponseMessage(CachedResponse cached) + internal DownstreamResponse CreateHttpResponseMessage(CachedResponse cached) { if (cached == null) { return null; } - var response = new HttpResponseMessage(cached.StatusCode); - - foreach (var header in cached.Headers) - { - response.Headers.Add(header.Key, header.Value); - } - var content = new MemoryStream(Convert.FromBase64String(cached.Body)); - response.Content = new StreamContent(content); + var streamContent = new StreamContent(content); foreach (var header in cached.ContentHeaders) { - response.Content.Headers.Add(header.Key, header.Value); + streamContent.Headers.Add(header.Key, header.Value); } - return response; + return new DownstreamResponse(streamContent, cached.StatusCode, cached.Headers.ToList()); } - internal async Task CreateCachedResponse(HttpResponseMessage response) + internal async Task CreateCachedResponse(DownstreamResponse response) { if (response == null) { @@ -112,7 +102,7 @@ namespace Ocelot.Cache.Middleware } var statusCode = response.StatusCode; - var headers = response.Headers.ToDictionary(v => v.Key, v => v.Value); + var headers = response.Headers.ToDictionary(v => v.Key, v => v.Values); string body = null; if (response.Content != null) diff --git a/src/Ocelot/Cache/OutputCacheController.cs b/src/Ocelot/Cache/OutputCacheController.cs index 32b9fa7c..cc33e8aa 100644 --- a/src/Ocelot/Cache/OutputCacheController.cs +++ b/src/Ocelot/Cache/OutputCacheController.cs @@ -1,9 +1,5 @@ -using System.Net.Http; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Ocelot.Cache; -using Ocelot.Configuration.Provider; namespace Ocelot.Cache { @@ -11,7 +7,7 @@ namespace Ocelot.Cache [Route("outputcache")] public class OutputCacheController : Controller { - private IOcelotCache _cache; + private readonly IOcelotCache _cache; public OutputCacheController(IOcelotCache cache) { @@ -26,4 +22,4 @@ namespace Ocelot.Cache return new NoContentResult(); } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs b/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs index 48f57834..2dfc3dc0 100644 --- a/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs +++ b/src/Ocelot/Claims/Middleware/ClaimsBuilderMiddleware.cs @@ -12,28 +12,27 @@ namespace Ocelot.Claims.Middleware { private readonly OcelotRequestDelegate _next; private readonly IAddClaimsToRequest _addClaimsToRequest; - private readonly IOcelotLogger _logger; public ClaimsBuilderMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IAddClaimsToRequest addClaimsToRequest) + :base(loggerFactory.CreateLogger()) { _next = next; _addClaimsToRequest = addClaimsToRequest; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) { if (context.DownstreamReRoute.ClaimsToClaims.Any()) { - _logger.LogDebug("this route has instructions to convert claims to other claims"); + Logger.LogDebug("this route has instructions to convert claims to other claims"); var result = _addClaimsToRequest.SetClaimsOnContext(context.DownstreamReRoute.ClaimsToClaims, context.HttpContext); if (result.IsError) { - _logger.LogDebug("error converting claims to other claims, setting pipeline error"); + Logger.LogDebug("error converting claims to other claims, setting pipeline error"); SetPipelineError(context, result.Errors); return; diff --git a/src/Ocelot/Configuration/Authentication/HashMatcher.cs b/src/Ocelot/Configuration/Authentication/HashMatcher.cs deleted file mode 100644 index 5f17362f..00000000 --- a/src/Ocelot/Configuration/Authentication/HashMatcher.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using Microsoft.AspNetCore.Cryptography.KeyDerivation; - -namespace Ocelot.Configuration.Authentication -{ - public class HashMatcher : IHashMatcher - { - public bool Match(string password, string salt, string hash) - { - byte[] s = Convert.FromBase64String(salt); - - string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2( - password: password, - salt: s, - prf: KeyDerivationPrf.HMACSHA256, - iterationCount: 10000, - numBytesRequested: 256 / 8)); - - return hashed == hash; - } - } -} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Authentication/IHashMatcher.cs b/src/Ocelot/Configuration/Authentication/IHashMatcher.cs deleted file mode 100644 index 629bf008..00000000 --- a/src/Ocelot/Configuration/Authentication/IHashMatcher.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Ocelot.Configuration.Authentication -{ - public interface IHashMatcher - { - bool Match(string password, string salt, string hash); - } -} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs index 0e28aaa7..15fddb97 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamReRouteBuilder.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net.Http; using Ocelot.Values; using System.Linq; +using Ocelot.Configuration.Creator; namespace Ocelot.Configuration.Builder { @@ -37,11 +38,16 @@ namespace Ocelot.Configuration.Builder private string _upstreamHost; private string _key; private List _delegatingHandlers; + private List _addHeadersToDownstream; + private List _addHeadersToUpstream; + private bool _dangerousAcceptAnyServerCertificateValidator; public DownstreamReRouteBuilder() { _downstreamAddresses = new List(); _delegatingHandlers = new List(); + _addHeadersToDownstream = new List(); + _addHeadersToUpstream = new List(); } public DownstreamReRouteBuilder WithDownstreamAddresses(List downstreamAddresses) @@ -224,6 +230,24 @@ namespace Ocelot.Configuration.Builder return this; } + public DownstreamReRouteBuilder WithAddHeadersToDownstream(List addHeadersToDownstream) + { + _addHeadersToDownstream = addHeadersToDownstream; + return this; + } + + public DownstreamReRouteBuilder WithAddHeadersToUpstream(List addHeadersToUpstream) + { + _addHeadersToUpstream = addHeadersToUpstream; + return this; + } + + public DownstreamReRouteBuilder WithDangerousAcceptAnyServerCertificateValidator(bool dangerousAcceptAnyServerCertificateValidator) + { + _dangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; + return this; + } + public DownstreamReRoute Build() { return new DownstreamReRoute( @@ -253,7 +277,10 @@ namespace Ocelot.Configuration.Builder _authenticationOptions, new PathTemplate(_downstreamPathTemplate), _reRouteKey, - _delegatingHandlers); + _delegatingHandlers, + _addHeadersToDownstream, + _addHeadersToUpstream, + _dangerousAcceptAnyServerCertificateValidator); } } } diff --git a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs index 0d059932..31367f26 100644 --- a/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ReRouteBuilder.cs @@ -14,6 +14,7 @@ namespace Ocelot.Configuration.Builder private List _upstreamHttpMethod; private string _upstreamHost; private List _downstreamReRoutes; + private string _aggregator; public ReRouteBuilder() { @@ -56,6 +57,12 @@ namespace Ocelot.Configuration.Builder return this; } + public ReRouteBuilder WithAggregator(string aggregator) + { + _aggregator = aggregator; + return this; + } + public ReRoute Build() { return new ReRoute( @@ -63,7 +70,8 @@ namespace Ocelot.Configuration.Builder new PathTemplate(_upstreamTemplate), _upstreamHttpMethod, _upstreamTemplatePattern, - _upstreamHost + _upstreamHost, + _aggregator ); } } diff --git a/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs b/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs index b1f4e832..cd8446a5 100644 --- a/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs +++ b/src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs @@ -5,28 +5,35 @@ namespace Ocelot.Configuration.Builder private string _serviceDiscoveryProviderHost; private int _serviceDiscoveryProviderPort; private string _type; + private string _token; - public ServiceProviderConfigurationBuilder WithServiceDiscoveryProviderHost(string serviceDiscoveryProviderHost) + public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost) { _serviceDiscoveryProviderHost = serviceDiscoveryProviderHost; return this; } - public ServiceProviderConfigurationBuilder WithServiceDiscoveryProviderPort(int serviceDiscoveryProviderPort) + public ServiceProviderConfigurationBuilder WithPort(int serviceDiscoveryProviderPort) { _serviceDiscoveryProviderPort = serviceDiscoveryProviderPort; return this; } - public ServiceProviderConfigurationBuilder WithServiceDiscoveryProviderType(string type) + public ServiceProviderConfigurationBuilder WithType(string type) { _type = type; return this; } + public ServiceProviderConfigurationBuilder WithToken(string token) + { + _token = token; + return this; + } + public ServiceProviderConfiguration Build() { - return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort); + return new ServiceProviderConfiguration(_type, _serviceDiscoveryProviderHost, _serviceDiscoveryProviderPort, _token); } } } diff --git a/src/Ocelot/Configuration/Creator/AddHeader.cs b/src/Ocelot/Configuration/Creator/AddHeader.cs new file mode 100644 index 00000000..72cae161 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/AddHeader.cs @@ -0,0 +1,14 @@ +namespace Ocelot.Configuration.Creator +{ + public class AddHeader + { + public AddHeader(string key, string value) + { + Key = key; + Value = value; + } + + public string Key { get; } + public string Value { get; } + } +} diff --git a/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs b/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs index 985abae4..0be61658 100644 --- a/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs +++ b/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs @@ -26,8 +26,7 @@ namespace Ocelot.Configuration.Creator if (claimToThing.IsError) { - _logger.LogDebug("ClaimsToThingCreator.BuildAddThingsToRequest", - $"Unable to extract configuration for key: {input.Key} and value: {input.Value} your configuration file is incorrect"); + _logger.LogDebug($"Unable to extract configuration for key: {input.Key} and value: {input.Value} your configuration file is incorrect"); } else { diff --git a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs similarity index 88% rename from src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs rename to src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs index 94cde717..46eec9d6 100644 --- a/src/Ocelot/Configuration/Creator/FileOcelotConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs @@ -16,9 +16,8 @@ namespace Ocelot.Configuration.Creator /// /// Register as singleton /// - public class FileOcelotConfigurationCreator : IOcelotConfigurationCreator + public class FileInternalConfigurationCreator : IInternalConfigurationCreator { - private readonly IOptions _options; private readonly IConfigurationValidator _configurationValidator; private readonly IOcelotLogger _logger; private readonly IClaimsToThingCreator _claimsToThingCreator; @@ -35,8 +34,7 @@ namespace Ocelot.Configuration.Creator private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; - public FileOcelotConfigurationCreator( - IOptions options, + public FileInternalConfigurationCreator( IConfigurationValidator configurationValidator, IOcelotLoggerFactory loggerFactory, IClaimsToThingCreator claimsToThingCreator, @@ -62,9 +60,8 @@ namespace Ocelot.Configuration.Creator _requestIdKeyCreator = requestIdKeyCreator; _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; _authOptionsCreator = authOptionsCreator; - _options = options; _configurationValidator = configurationValidator; - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); _claimsToThingCreator = claimsToThingCreator; _serviceProviderConfigCreator = serviceProviderConfigCreator; _qosOptionsCreator = qosOptionsCreator; @@ -72,19 +69,19 @@ namespace Ocelot.Configuration.Creator _httpHandlerOptionsCreator = httpHandlerOptionsCreator; } - public async Task> Create(FileConfiguration fileConfiguration) + public async Task> Create(FileConfiguration fileConfiguration) { var config = await SetUpConfiguration(fileConfiguration); return config; } - private async Task> SetUpConfiguration(FileConfiguration fileConfiguration) + private async Task> SetUpConfiguration(FileConfiguration fileConfiguration) { var response = await _configurationValidator.IsValid(fileConfiguration); if (response.Data.IsError) { - return new ErrorResponse(response.Data.Errors); + return new ErrorResponse(response.Data.Errors); } var reRoutes = new List(); @@ -106,9 +103,9 @@ namespace Ocelot.Configuration.Creator var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(fileConfiguration.GlobalConfiguration); - var config = new OcelotConfiguration(reRoutes, _adminPath.Path, serviceProviderConfiguration, fileConfiguration.GlobalConfiguration.RequestIdKey); + var config = new InternalConfiguration(reRoutes, _adminPath.Path, serviceProviderConfiguration, fileConfiguration.GlobalConfiguration.RequestIdKey); - return new OkResponse(config); + return new OkResponse(config); } public ReRoute SetUpAggregateReRoute(List reRoutes, FileAggregateReRoute aggregateReRoute, FileGlobalConfiguration globalConfiguration) @@ -132,6 +129,7 @@ namespace Ocelot.Configuration.Creator .WithUpstreamTemplatePattern(upstreamTemplatePattern) .WithDownstreamReRoutes(applicableReRoutes) .WithUpstreamHost(aggregateReRoute.UpstreamHost) + .WithAggregator(aggregateReRoute.Aggregator) .Build(); return reRoute; @@ -213,6 +211,9 @@ namespace Ocelot.Configuration.Creator .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) .WithUpstreamHost(fileReRoute.UpstreamHost) .WithDelegatingHandlers(fileReRoute.DelegatingHandlers) + .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream) + .WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream) + .WithDangerousAcceptAnyServerCertificateValidator(fileReRoute.DangerousAcceptAnyServerCertificateValidator) .Build(); return reRoute; diff --git a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs index 7ab33e90..1a5f1b6a 100644 --- a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs +++ b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs @@ -1,44 +1,76 @@ using System; using System.Collections.Generic; using Ocelot.Configuration.File; +using Ocelot.Infrastructure; +using Ocelot.Logging; using Ocelot.Middleware; +using Ocelot.Responses; namespace Ocelot.Configuration.Creator { public class HeaderFindAndReplaceCreator : IHeaderFindAndReplaceCreator { - private IBaseUrlFinder _finder; - private readonly Dictionary> _placeholders; + private readonly IPlaceholders _placeholders; + private readonly IOcelotLogger _logger; - public HeaderFindAndReplaceCreator(IBaseUrlFinder finder) + public HeaderFindAndReplaceCreator(IPlaceholders placeholders, IOcelotLoggerFactory factory) { - _finder = finder; - _placeholders = new Dictionary>(); - _placeholders.Add("{BaseUrl}", () => _finder.Find()); + _logger = factory.CreateLogger(); + _placeholders = placeholders; } public HeaderTransformations Create(FileReRoute fileReRoute) { var upstream = new List(); + var addHeadersToUpstream = new List(); foreach(var input in fileReRoute.UpstreamHeaderTransform) { - var hAndr = Map(input); - upstream.Add(hAndr); + if (input.Value.Contains(",")) + { + var hAndr = Map(input); + if (!hAndr.IsError) + { + upstream.Add(hAndr.Data); + } + else + { + _logger.LogWarning($"Unable to add UpstreamHeaderTransform {input.Key}: {input.Value}"); + } + } + else + { + addHeadersToUpstream.Add(new AddHeader(input.Key, input.Value)); + } } var downstream = new List(); - + var addHeadersToDownstream = new List(); + foreach(var input in fileReRoute.DownstreamHeaderTransform) { - var hAndr = Map(input); - downstream.Add(hAndr); + if(input.Value.Contains(",")) + { + var hAndr = Map(input); + if(!hAndr.IsError) + { + downstream.Add(hAndr.Data); + } + else + { + _logger.LogWarning($"Unable to add DownstreamHeaderTransform {input.Key}: {input.Value}"); + } + } + else + { + addHeadersToDownstream.Add(new AddHeader(input.Key, input.Value)); + } } - return new HeaderTransformations(upstream, downstream); + return new HeaderTransformations(upstream, downstream, addHeadersToDownstream, addHeadersToUpstream); } - private HeaderFindAndReplace Map(KeyValuePair input) + private Response Map(KeyValuePair input) { var findAndReplace = input.Value.Split(","); @@ -51,16 +83,19 @@ namespace Ocelot.Configuration.Creator var placeholder = replace.Substring(startOfPlaceholder, startOfPlaceholder + (endOfPlaceholder + 1)); - if(_placeholders.ContainsKey(placeholder)) + var value = _placeholders.Get(placeholder); + + if(value.IsError) { - var value = _placeholders[placeholder].Invoke(); - replace = replace.Replace(placeholder, value); + return new ErrorResponse(value.Errors); } + + replace = replace.Replace(placeholder, value.Data); } var hAndr = new HeaderFindAndReplace(input.Key, findAndReplace[0], replace, 0); - return hAndr; + return new OkResponse(hAndr); } } } diff --git a/src/Ocelot/Configuration/Creator/HeaderTransformations.cs b/src/Ocelot/Configuration/Creator/HeaderTransformations.cs index 55e1e3b9..72b6e1f6 100644 --- a/src/Ocelot/Configuration/Creator/HeaderTransformations.cs +++ b/src/Ocelot/Configuration/Creator/HeaderTransformations.cs @@ -4,14 +4,23 @@ namespace Ocelot.Configuration.Creator { public class HeaderTransformations { - public HeaderTransformations(List upstream, List downstream) + public HeaderTransformations( + List upstream, + List downstream, + List addHeaderToDownstream, + List addHeaderToUpstream) { + AddHeadersToDownstream = addHeaderToDownstream; + AddHeadersToUpstream = addHeaderToUpstream; Upstream = upstream; Downstream = downstream; } - public List Upstream {get;private set;} + public List Upstream { get; } - public List Downstream {get;private set;} + public List Downstream { get; } + + public List AddHeadersToDownstream { get; } + public List AddHeadersToUpstream { get; } } } diff --git a/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs b/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs index 6c66f3c0..332c25b7 100644 --- a/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs +++ b/src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs @@ -1,13 +1,24 @@ -using Ocelot.Configuration.File; +using Butterfly.Client.Tracing; +using Ocelot.Configuration.File; +using Ocelot.Requester; namespace Ocelot.Configuration.Creator { public class HttpHandlerOptionsCreator : IHttpHandlerOptionsCreator { + private IServiceTracer _tracer; + + public HttpHandlerOptionsCreator(IServiceTracer tracer) + { + _tracer = tracer; + } + public HttpHandlerOptions Create(FileReRoute fileReRoute) { + var useTracing = _tracer.GetType() != typeof(FakeServiceTracer) ? fileReRoute.HttpHandlerOptions.UseTracing : false; + return new HttpHandlerOptions(fileReRoute.HttpHandlerOptions.AllowAutoRedirect, - fileReRoute.HttpHandlerOptions.UseCookieContainer, fileReRoute.HttpHandlerOptions.UseTracing); + fileReRoute.HttpHandlerOptions.UseCookieContainer, useTracing); } } } diff --git a/src/Ocelot/Configuration/Creator/IInternalConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IInternalConfigurationCreator.cs new file mode 100644 index 00000000..a0f3cb42 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IInternalConfigurationCreator.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Ocelot.Configuration.File; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Creator +{ + public interface IInternalConfigurationCreator + { + Task> Create(FileConfiguration fileConfiguration); + } +} diff --git a/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs deleted file mode 100644 index 4b431701..00000000 --- a/src/Ocelot/Configuration/Creator/IOcelotConfigurationCreator.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; -using Ocelot.Configuration.File; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Creator -{ - public interface IOcelotConfigurationCreator - { - Task> Create(FileConfiguration fileConfiguration); - } -} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs index 5e7fcd37..8569001e 100644 --- a/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/IdentityServerConfigurationCreator.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using IdentityServer4.AccessTokenValidation; -using IdentityServer4.Models; -using Ocelot.Configuration.Provider; namespace Ocelot.Configuration.Creator { diff --git a/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs b/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs index c104385a..aa735f1a 100644 --- a/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs +++ b/src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs @@ -11,9 +11,10 @@ namespace Ocelot.Configuration.Creator var serviceProviderPort = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; return new ServiceProviderConfigurationBuilder() - .WithServiceDiscoveryProviderHost(globalConfiguration?.ServiceDiscoveryProvider?.Host) - .WithServiceDiscoveryProviderPort(serviceProviderPort) - .WithServiceDiscoveryProviderType(globalConfiguration?.ServiceDiscoveryProvider?.Type) + .WithHost(globalConfiguration?.ServiceDiscoveryProvider?.Host) + .WithPort(serviceProviderPort) + .WithType(globalConfiguration?.ServiceDiscoveryProvider?.Type) + .WithToken(globalConfiguration?.ServiceDiscoveryProvider?.Token) .Build(); } } diff --git a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs index 833c61db..dbf6a2d1 100644 --- a/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs +++ b/src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs @@ -42,7 +42,7 @@ namespace Ocelot.Configuration.Creator if (upstreamTemplate == "/") { - return new UpstreamPathTemplate(RegExForwardSlashOnly, 1); + return new UpstreamPathTemplate(RegExForwardSlashOnly, reRoute.Priority); } if(upstreamTemplate.EndsWith("/")) @@ -54,7 +54,7 @@ namespace Ocelot.Configuration.Creator ? $"^{upstreamTemplate}{RegExMatchEndString}" : $"^{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; - return new UpstreamPathTemplate(route, 1); + return new UpstreamPathTemplate(route, reRoute.Priority); } private bool ForwardSlashAndOnePlaceHolder(string upstreamTemplate, List placeholders, int postitionOfPlaceHolderClosingBracket) diff --git a/src/Ocelot/Configuration/DownstreamReRoute.cs b/src/Ocelot/Configuration/DownstreamReRoute.cs index 6c9aa1dd..4fe89f22 100644 --- a/src/Ocelot/Configuration/DownstreamReRoute.cs +++ b/src/Ocelot/Configuration/DownstreamReRoute.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Ocelot.Configuration.Creator; using Ocelot.Values; namespace Ocelot.Configuration @@ -32,8 +33,13 @@ namespace Ocelot.Configuration AuthenticationOptions authenticationOptions, PathTemplate downstreamPathTemplate, string reRouteKey, - List delegatingHandlers) + List delegatingHandlers, + List addHeadersToDownstream, + List addHeadersToUpstream, + bool dangerousAcceptAnyServerCertificateValidator) { + DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; + AddHeadersToDownstream = addHeadersToDownstream; DelegatingHandlers = delegatingHandlers; Key = key; UpstreamPathTemplate = upstreamPathTemplate; @@ -61,6 +67,7 @@ namespace Ocelot.Configuration AuthenticationOptions = authenticationOptions; DownstreamPathTemplate = downstreamPathTemplate; ReRouteKey = reRouteKey; + AddHeadersToUpstream = addHeadersToUpstream; } public string Key { get; private set; } @@ -90,5 +97,8 @@ namespace Ocelot.Configuration public PathTemplate DownstreamPathTemplate { get; private set; } public string ReRouteKey { get; private set; } public List DelegatingHandlers {get;private set;} + public List AddHeadersToDownstream {get;private set;} + public List AddHeadersToUpstream { get; private set; } + public bool DangerousAcceptAnyServerCertificateValidator { get; private set; } } } diff --git a/src/Ocelot/Configuration/File/FileAggregateReRoute.cs b/src/Ocelot/Configuration/File/FileAggregateReRoute.cs index 8c9eabba..c862094a 100644 --- a/src/Ocelot/Configuration/File/FileAggregateReRoute.cs +++ b/src/Ocelot/Configuration/File/FileAggregateReRoute.cs @@ -8,11 +8,14 @@ namespace Ocelot.Configuration.File public string UpstreamPathTemplate { get;set; } public string UpstreamHost { get; set; } public bool ReRouteIsCaseSensitive { get; set; } + public string Aggregator { get; set; } // Only supports GET..are you crazy!! POST, PUT WOULD BE CRAZY!! :) public List UpstreamHttpMethod { get { return new List {"Get"}; } } + + public int Priority {get;set;} = 1; } } diff --git a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs index 7f24b572..2934254c 100644 --- a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs +++ b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs @@ -4,8 +4,8 @@ { public FileHttpHandlerOptions() { - AllowAutoRedirect = true; - UseCookieContainer = true; + AllowAutoRedirect = false; + UseCookieContainer = false; } public bool AllowAutoRedirect { get; set; } diff --git a/src/Ocelot/Configuration/File/FileReRoute.cs b/src/Ocelot/Configuration/File/FileReRoute.cs index adc747f9..acc6572a 100644 --- a/src/Ocelot/Configuration/File/FileReRoute.cs +++ b/src/Ocelot/Configuration/File/FileReRoute.cs @@ -20,6 +20,7 @@ namespace Ocelot.Configuration.File UpstreamHeaderTransform = new Dictionary(); DownstreamHostAndPorts = new List(); DelegatingHandlers = new List(); + Priority = 1; } public string DownstreamPathTemplate { get; set; } @@ -46,5 +47,8 @@ namespace Ocelot.Configuration.File public string UpstreamHost { get; set; } public string Key { get;set; } public List DelegatingHandlers {get;set;} + public int Priority { get;set; } + public int Timeout { get; set; } + public bool DangerousAcceptAnyServerCertificateValidator { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs index 203cc675..9a96a6d3 100644 --- a/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs +++ b/src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs @@ -5,5 +5,6 @@ namespace Ocelot.Configuration.File public string Host {get;set;} public int Port { get; set; } public string Type { get; set; } + public string Token { get; set; } } } diff --git a/src/Ocelot/Configuration/File/IReRoute.cs b/src/Ocelot/Configuration/File/IReRoute.cs index 69128d3a..fb7e9313 100644 --- a/src/Ocelot/Configuration/File/IReRoute.cs +++ b/src/Ocelot/Configuration/File/IReRoute.cs @@ -4,5 +4,6 @@ { string UpstreamPathTemplate { get; set; } bool ReRouteIsCaseSensitive { get; set; } + int Priority {get;set;} } } diff --git a/src/Ocelot/Configuration/FileConfigurationController.cs b/src/Ocelot/Configuration/FileConfigurationController.cs index 7bdf4926..707eb61d 100644 --- a/src/Ocelot/Configuration/FileConfigurationController.cs +++ b/src/Ocelot/Configuration/FileConfigurationController.cs @@ -2,34 +2,34 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.File; -using Ocelot.Configuration.Provider; using Ocelot.Configuration.Setter; using Ocelot.Raft; using Rafty.Concensus; namespace Ocelot.Configuration { + using Repository; + [Authorize] [Route("configuration")] public class FileConfigurationController : Controller { - private readonly IFileConfigurationProvider _configGetter; - private readonly IFileConfigurationSetter _configSetter; - private readonly IServiceProvider _serviceProvider; + private readonly IFileConfigurationRepository _repo; + private readonly IFileConfigurationSetter _setter; + private readonly IServiceProvider _provider; - public FileConfigurationController(IFileConfigurationProvider getFileConfig, IFileConfigurationSetter configSetter, IServiceProvider serviceProvider) + public FileConfigurationController(IFileConfigurationRepository repo, IFileConfigurationSetter setter, IServiceProvider provider) { - _configGetter = getFileConfig; - _configSetter = configSetter; - _serviceProvider = serviceProvider; + _repo = repo; + _setter = setter; + _provider = provider; } [HttpGet] public async Task Get() { - var response = await _configGetter.Get(); + var response = await _repo.Get(); if(response.IsError) { @@ -43,7 +43,7 @@ namespace Ocelot.Configuration public async Task Post([FromBody]FileConfiguration fileConfiguration) { //todo - this code is a bit shit sort it out.. - var test = _serviceProvider.GetService(typeof(INode)); + var test = _provider.GetService(typeof(INode)); if (test != null) { var node = (INode)test; @@ -56,7 +56,7 @@ namespace Ocelot.Configuration return new OkObjectResult(result.Command.Configuration); } - var response = await _configSetter.Set(fileConfiguration); + var response = await _setter.Set(fileConfiguration); if (response.IsError) { diff --git a/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs b/src/Ocelot/Configuration/IIdentityServerConfiguration.cs similarity index 67% rename from src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs rename to src/Ocelot/Configuration/IIdentityServerConfiguration.cs index 8a76eb9f..0eb70347 100644 --- a/src/Ocelot/Configuration/Provider/IIdentityServerConfiguration.cs +++ b/src/Ocelot/Configuration/IIdentityServerConfiguration.cs @@ -1,16 +1,14 @@ -using System.Collections.Generic; -using IdentityServer4.AccessTokenValidation; -using IdentityServer4.Models; - -namespace Ocelot.Configuration.Provider -{ - public interface IIdentityServerConfiguration - { - string ApiName { get; } - string ApiSecret { get; } - bool RequireHttps { get; } - List AllowedScopes { get; } - string CredentialsSigningCertificateLocation { get; } - string CredentialsSigningCertificatePassword { get; } - } -} \ No newline at end of file +namespace Ocelot.Configuration +{ + using System.Collections.Generic; + + public interface IIdentityServerConfiguration + { + string ApiName { get; } + string ApiSecret { get; } + bool RequireHttps { get; } + List AllowedScopes { get; } + string CredentialsSigningCertificateLocation { get; } + string CredentialsSigningCertificatePassword { get; } + } +} diff --git a/src/Ocelot/Configuration/IOcelotConfiguration.cs b/src/Ocelot/Configuration/IInternalConfiguration.cs similarity index 83% rename from src/Ocelot/Configuration/IOcelotConfiguration.cs rename to src/Ocelot/Configuration/IInternalConfiguration.cs index fd808ae4..c1781c48 100644 --- a/src/Ocelot/Configuration/IOcelotConfiguration.cs +++ b/src/Ocelot/Configuration/IInternalConfiguration.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; - -namespace Ocelot.Configuration -{ - public interface IOcelotConfiguration - { - List ReRoutes { get; } - string AdministrationPath {get;} - ServiceProviderConfiguration ServiceProviderConfiguration {get;} - string RequestId {get;} - } -} \ No newline at end of file +using System.Collections.Generic; + +namespace Ocelot.Configuration +{ + public interface IInternalConfiguration + { + List ReRoutes { get; } + string AdministrationPath {get;} + ServiceProviderConfiguration ServiceProviderConfiguration {get;} + string RequestId {get;} + } +} diff --git a/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs b/src/Ocelot/Configuration/IdentityServerConfiguration.cs similarity index 66% rename from src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs rename to src/Ocelot/Configuration/IdentityServerConfiguration.cs index 795e6994..b8b00ea2 100644 --- a/src/Ocelot/Configuration/Provider/IdentityServerConfiguration.cs +++ b/src/Ocelot/Configuration/IdentityServerConfiguration.cs @@ -1,32 +1,30 @@ -using System.Collections.Generic; -using IdentityServer4.AccessTokenValidation; -using IdentityServer4.Models; - -namespace Ocelot.Configuration.Provider -{ - public class IdentityServerConfiguration : IIdentityServerConfiguration - { - public IdentityServerConfiguration( - string apiName, - bool requireHttps, - string apiSecret, - List allowedScopes, - string credentialsSigningCertificateLocation, - string credentialsSigningCertificatePassword) - { - ApiName = apiName; - RequireHttps = requireHttps; - ApiSecret = apiSecret; - AllowedScopes = allowedScopes; - CredentialsSigningCertificateLocation = credentialsSigningCertificateLocation; - CredentialsSigningCertificatePassword = credentialsSigningCertificatePassword; - } - - public string ApiName { get; private set; } - public bool RequireHttps { get; private set; } - public List AllowedScopes { get; private set; } - public string ApiSecret { get; private set; } - public string CredentialsSigningCertificateLocation { get; private set; } - public string CredentialsSigningCertificatePassword { get; private set; } - } -} \ No newline at end of file +namespace Ocelot.Configuration +{ + using System.Collections.Generic; + + public class IdentityServerConfiguration : IIdentityServerConfiguration + { + public IdentityServerConfiguration( + string apiName, + bool requireHttps, + string apiSecret, + List allowedScopes, + string credentialsSigningCertificateLocation, + string credentialsSigningCertificatePassword) + { + ApiName = apiName; + RequireHttps = requireHttps; + ApiSecret = apiSecret; + AllowedScopes = allowedScopes; + CredentialsSigningCertificateLocation = credentialsSigningCertificateLocation; + CredentialsSigningCertificatePassword = credentialsSigningCertificatePassword; + } + + public string ApiName { get; } + public bool RequireHttps { get; } + public List AllowedScopes { get; } + public string ApiSecret { get; } + public string CredentialsSigningCertificateLocation { get; } + public string CredentialsSigningCertificatePassword { get; } + } +} diff --git a/src/Ocelot/Configuration/OcelotConfiguration.cs b/src/Ocelot/Configuration/InternalConfiguration.cs similarity index 67% rename from src/Ocelot/Configuration/OcelotConfiguration.cs rename to src/Ocelot/Configuration/InternalConfiguration.cs index 1ab73b87..429bb9c0 100644 --- a/src/Ocelot/Configuration/OcelotConfiguration.cs +++ b/src/Ocelot/Configuration/InternalConfiguration.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; - -namespace Ocelot.Configuration -{ - public class OcelotConfiguration : IOcelotConfiguration - { - public OcelotConfiguration(List reRoutes, string administrationPath, ServiceProviderConfiguration serviceProviderConfiguration, string requestId) - { - ReRoutes = reRoutes; - AdministrationPath = administrationPath; - ServiceProviderConfiguration = serviceProviderConfiguration; - RequestId = requestId; - } - - public List ReRoutes { get; } - public string AdministrationPath {get;} - public ServiceProviderConfiguration ServiceProviderConfiguration {get;} - public string RequestId {get;} - } -} +using System.Collections.Generic; + +namespace Ocelot.Configuration +{ + public class InternalConfiguration : IInternalConfiguration + { + public InternalConfiguration(List reRoutes, string administrationPath, ServiceProviderConfiguration serviceProviderConfiguration, string requestId) + { + ReRoutes = reRoutes; + AdministrationPath = administrationPath; + ServiceProviderConfiguration = serviceProviderConfiguration; + RequestId = requestId; + } + + public List ReRoutes { get; } + public string AdministrationPath {get;} + public ServiceProviderConfiguration ServiceProviderConfiguration {get;} + public string RequestId {get;} + } +} diff --git a/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs b/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs index 4d698e96..94df168f 100644 --- a/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs +++ b/src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs @@ -20,22 +20,14 @@ namespace Ocelot.Configuration.Parser if (instructions.Length <= 1) { - return new ErrorResponse( - new List - { - new NoInstructionsError(SplitToken) - }); + return new ErrorResponse(new NoInstructionsError(SplitToken)); } var claimMatch = _claimRegex.IsMatch(instructions[0]); if (!claimMatch) { - return new ErrorResponse( - new List - { - new InstructionNotForClaimsError() - }); + return new ErrorResponse(new InstructionNotForClaimsError()); } var newKey = GetIndexValue(instructions[0]); @@ -53,11 +45,7 @@ namespace Ocelot.Configuration.Parser } catch (Exception exception) { - return new ErrorResponse( - new List - { - new ParsingConfigurationHeaderError(exception) - }); + return new ErrorResponse(new ParsingConfigurationHeaderError(exception)); } } diff --git a/src/Ocelot/Configuration/Provider/FileConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/FileConfigurationProvider.cs deleted file mode 100644 index fdcd949b..00000000 --- a/src/Ocelot/Configuration/Provider/FileConfigurationProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Repository; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Provider -{ - public class FileConfigurationProvider : IFileConfigurationProvider - { - private IFileConfigurationRepository _repo; - - public FileConfigurationProvider(IFileConfigurationRepository repo) - { - _repo = repo; - } - - public async Task> Get() - { - var fileConfig = await _repo.Get(); - return new OkResponse(fileConfig.Data); - } - } -} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/IFileConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/IFileConfigurationProvider.cs deleted file mode 100644 index c2ab51cb..00000000 --- a/src/Ocelot/Configuration/Provider/IFileConfigurationProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; -using Ocelot.Configuration.File; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Provider -{ - public interface IFileConfigurationProvider - { - Task> Get(); - } -} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Provider/IOcelotConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/IOcelotConfigurationProvider.cs deleted file mode 100644 index 80f4583b..00000000 --- a/src/Ocelot/Configuration/Provider/IOcelotConfigurationProvider.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; -using Ocelot.Configuration.File; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Provider -{ - public interface IOcelotConfigurationProvider - { - Task> Get(); - } -} diff --git a/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs b/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs deleted file mode 100644 index ed72a60e..00000000 --- a/src/Ocelot/Configuration/Provider/OcelotConfigurationProvider.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Threading.Tasks; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Repository; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Provider -{ - /// - /// Register as singleton - /// - public class OcelotConfigurationProvider : IOcelotConfigurationProvider - { - private readonly IOcelotConfigurationRepository _config; - - public OcelotConfigurationProvider(IOcelotConfigurationRepository repo) - { - _config = repo; - } - - public async Task> Get() - { - var repoConfig = await _config.Get(); - - if (repoConfig.IsError) - { - return new ErrorResponse(repoConfig.Errors); - } - - return new OkResponse(repoConfig.Data); - } - } -} \ No newline at end of file diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index 651bd506..8c5d6d27 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -16,12 +16,12 @@ namespace Ocelot.Configuration TimeoutStrategy = timeoutStrategy; } - public int ExceptionsAllowedBeforeBreaking { get; private set; } + public int ExceptionsAllowedBeforeBreaking { get; } - public int DurationOfBreak { get; private set; } + public int DurationOfBreak { get; } - public int TimeoutValue { get; private set; } + public int TimeoutValue { get; } - public TimeoutStrategy TimeoutStrategy { get; private set; } + public TimeoutStrategy TimeoutStrategy { get; } } } diff --git a/src/Ocelot/Configuration/ReRoute.cs b/src/Ocelot/Configuration/ReRoute.cs index 47f26291..c4c3952d 100644 --- a/src/Ocelot/Configuration/ReRoute.cs +++ b/src/Ocelot/Configuration/ReRoute.cs @@ -12,13 +12,15 @@ namespace Ocelot.Configuration PathTemplate upstreamPathTemplate, List upstreamHttpMethod, UpstreamPathTemplate upstreamTemplatePattern, - string upstreamHost) + string upstreamHost, + string aggregator) { UpstreamHost = upstreamHost; DownstreamReRoute = downstreamReRoute; UpstreamPathTemplate = upstreamPathTemplate; UpstreamHttpMethod = upstreamHttpMethod; UpstreamTemplatePattern = upstreamTemplatePattern; + Aggregator = aggregator; } public PathTemplate UpstreamPathTemplate { get; private set; } @@ -26,5 +28,6 @@ namespace Ocelot.Configuration public List UpstreamHttpMethod { get; private set; } public string UpstreamHost { get; private set; } public List DownstreamReRoute { get; private set; } + public string Aggregator {get; private set;} } } diff --git a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs index a2780062..16108d15 100644 --- a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs +++ b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationPoller.cs @@ -17,10 +17,16 @@ namespace Ocelot.Configuration.Repository private string _previousAsJson; private readonly Timer _timer; private bool _polling; + private readonly IConsulPollerConfiguration _config; - public ConsulFileConfigurationPoller(IOcelotLoggerFactory factory, IFileConfigurationRepository repo, IFileConfigurationSetter setter) + public ConsulFileConfigurationPoller( + IOcelotLoggerFactory factory, + IFileConfigurationRepository repo, + IFileConfigurationSetter setter, + IConsulPollerConfiguration config) { _setter = setter; + _config = config; _logger = factory.CreateLogger(); _repo = repo; _previousAsJson = ""; @@ -30,22 +36,22 @@ namespace Ocelot.Configuration.Repository { return; } - + _polling = true; await Poll(); _polling = false; - }, null, 0, 1000); + }, null, 0, _config.Delay); } private async Task Poll() { - _logger.LogDebug("Started polling consul"); + _logger.LogInformation("Started polling consul"); var fileConfig = await _repo.Get(); if(fileConfig.IsError) { - _logger.LogDebug($"error geting file config, errors are {string.Join(",", fileConfig.Errors.Select(x => x.Message))}"); + _logger.LogWarning($"error geting file config, errors are {string.Join(",", fileConfig.Errors.Select(x => x.Message))}"); return; } @@ -57,14 +63,13 @@ namespace Ocelot.Configuration.Repository _previousAsJson = asJson; } - _logger.LogDebug("Finished polling consul"); + _logger.LogInformation("Finished polling consul"); } /// /// We could do object comparison here but performance isnt really a problem. This might be an issue one day! /// - /// - /// + /// hash of the config private string ToJson(FileConfiguration config) { var currentHash = JsonConvert.SerializeObject(config); diff --git a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationRepository.cs index 97bf7c97..165e035b 100644 --- a/src/Ocelot/Configuration/Repository/ConsulFileConfigurationRepository.cs +++ b/src/Ocelot/Configuration/Repository/ConsulFileConfigurationRepository.cs @@ -1,42 +1,60 @@ -using System; -using System.Text; -using System.Threading.Tasks; -using Consul; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using Ocelot.Responses; -using Ocelot.ServiceDiscovery; - namespace Ocelot.Configuration.Repository { + using System; + using System.Text; + using System.Threading.Tasks; + using Consul; + using Newtonsoft.Json; + using Ocelot.Configuration.File; + using Ocelot.Infrastructure.Consul; + using Ocelot.Logging; + using Ocelot.Responses; + using Ocelot.ServiceDiscovery.Configuration; + public class ConsulFileConfigurationRepository : IFileConfigurationRepository { - private readonly ConsulClient _consul; - private string _ocelotConfiguration = "OcelotConfiguration"; + private readonly IConsulClient _consul; + private const string OcelotConfiguration = "InternalConfiguration"; private readonly Cache.IOcelotCache _cache; + private readonly IOcelotLogger _logger; - public ConsulFileConfigurationRepository(Cache.IOcelotCache cache, ServiceProviderConfiguration serviceProviderConfig) + public ConsulFileConfigurationRepository( + Cache.IOcelotCache cache, + IInternalConfigurationRepository repo, + IConsulClientFactory factory, + IOcelotLoggerFactory loggerFactory) { - var consulHost = string.IsNullOrEmpty(serviceProviderConfig?.Host) ? "localhost" : serviceProviderConfig?.Host; - var consulPort = serviceProviderConfig?.Port ?? 8500; - var configuration = new ConsulRegistryConfiguration(consulHost, consulPort, _ocelotConfiguration); + _logger = loggerFactory.CreateLogger(); _cache = cache; - _consul = new ConsulClient(c => + + var internalConfig = repo.Get(); + + var consulHost = "localhost"; + var consulPort = 8500; + string token = null; + + if (!internalConfig.IsError) { - c.Address = new Uri($"http://{configuration.HostName}:{configuration.Port}"); - }); + consulHost = string.IsNullOrEmpty(internalConfig.Data.ServiceProviderConfiguration?.Host) ? consulHost : internalConfig.Data.ServiceProviderConfiguration?.Host; + consulPort = internalConfig.Data.ServiceProviderConfiguration?.Port ?? consulPort; + token = internalConfig.Data.ServiceProviderConfiguration?.Token; + } + + var config = new ConsulRegistryConfiguration(consulHost, consulPort, OcelotConfiguration, token); + + _consul = factory.Get(config); } public async Task> Get() { - var config = _cache.Get(_ocelotConfiguration, _ocelotConfiguration); + var config = _cache.Get(OcelotConfiguration, OcelotConfiguration); if (config != null) { return new OkResponse(config); } - var queryResult = await _consul.KV.Get(_ocelotConfiguration); + var queryResult = await _consul.KV.Get(OcelotConfiguration); if (queryResult.Response == null) { @@ -54,11 +72,11 @@ namespace Ocelot.Configuration.Repository public async Task Set(FileConfiguration ocelotConfiguration) { - var json = JsonConvert.SerializeObject(ocelotConfiguration); + var json = JsonConvert.SerializeObject(ocelotConfiguration, Formatting.Indented); var bytes = Encoding.UTF8.GetBytes(json); - var kvPair = new KVPair(_ocelotConfiguration) + var kvPair = new KVPair(OcelotConfiguration) { Value = bytes }; @@ -67,7 +85,7 @@ namespace Ocelot.Configuration.Repository if (result.Response) { - _cache.AddAndDelete(_ocelotConfiguration, ocelotConfiguration, TimeSpan.FromSeconds(3), _ocelotConfiguration); + _cache.AddAndDelete(OcelotConfiguration, ocelotConfiguration, TimeSpan.FromSeconds(3), OcelotConfiguration); return new OkResponse(); } diff --git a/src/Ocelot/Configuration/Repository/FileConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs similarity index 73% rename from src/Ocelot/Configuration/Repository/FileConfigurationRepository.cs rename to src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs index ff5e3876..a870419c 100644 --- a/src/Ocelot/Configuration/Repository/FileConfigurationRepository.cs +++ b/src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs @@ -1,52 +1,54 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Repository -{ - public class FileConfigurationRepository : IFileConfigurationRepository - { - private readonly string _configFilePath; - - private static readonly object _lock = new object(); - - public FileConfigurationRepository(IHostingEnvironment hostingEnvironment) - { - _configFilePath = $"{AppContext.BaseDirectory}/configuration{(string.IsNullOrEmpty(hostingEnvironment.EnvironmentName) ? string.Empty : ".")}{hostingEnvironment.EnvironmentName}.json"; - } - - public Task> Get() - { - string jsonConfiguration; - - lock(_lock) - { - jsonConfiguration = System.IO.File.ReadAllText(_configFilePath); - } - - var fileConfiguration = JsonConvert.DeserializeObject(jsonConfiguration); - - return Task.FromResult>(new OkResponse(fileConfiguration)); - } - - public Task Set(FileConfiguration fileConfiguration) - { - string jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); - - lock(_lock) - { - if (System.IO.File.Exists(_configFilePath)) - { - System.IO.File.Delete(_configFilePath); - } - - System.IO.File.WriteAllText(_configFilePath, jsonConfiguration); - } - - return Task.FromResult(new OkResponse()); - } - } -} +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Repository +{ + public class DiskFileConfigurationRepository : IFileConfigurationRepository + { + private readonly string _configFilePath; + + private static readonly object _lock = new object(); + + private const string ConfigurationFileName = "ocelot"; + + public DiskFileConfigurationRepository(IHostingEnvironment hostingEnvironment) + { + _configFilePath = $"{AppContext.BaseDirectory}/{ConfigurationFileName}{(string.IsNullOrEmpty(hostingEnvironment.EnvironmentName) ? string.Empty : ".")}{hostingEnvironment.EnvironmentName}.json"; + } + + public Task> Get() + { + string jsonConfiguration; + + lock(_lock) + { + jsonConfiguration = System.IO.File.ReadAllText(_configFilePath); + } + + var fileConfiguration = JsonConvert.DeserializeObject(jsonConfiguration); + + return Task.FromResult>(new OkResponse(fileConfiguration)); + } + + public Task Set(FileConfiguration fileConfiguration) + { + string jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); + + lock(_lock) + { + if (System.IO.File.Exists(_configFilePath)) + { + System.IO.File.Delete(_configFilePath); + } + + System.IO.File.WriteAllText(_configFilePath, jsonConfiguration); + } + + return Task.FromResult(new OkResponse()); + } + } +} diff --git a/src/Ocelot/Configuration/Repository/IConsulPollerConfiguration.cs b/src/Ocelot/Configuration/Repository/IConsulPollerConfiguration.cs new file mode 100644 index 00000000..d1f1430d --- /dev/null +++ b/src/Ocelot/Configuration/Repository/IConsulPollerConfiguration.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Configuration.Repository +{ + public interface IConsulPollerConfiguration + { + int Delay { get; } + } +} diff --git a/src/Ocelot/Configuration/Repository/IInternalConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/IInternalConfigurationRepository.cs new file mode 100644 index 00000000..5db4adb5 --- /dev/null +++ b/src/Ocelot/Configuration/Repository/IInternalConfigurationRepository.cs @@ -0,0 +1,10 @@ +using Ocelot.Responses; + +namespace Ocelot.Configuration.Repository +{ + public interface IInternalConfigurationRepository + { + Response Get(); + Response AddOrReplace(IInternalConfiguration internalConfiguration); + } +} diff --git a/src/Ocelot/Configuration/Repository/IOcelotConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/IOcelotConfigurationRepository.cs deleted file mode 100644 index 16b386a1..00000000 --- a/src/Ocelot/Configuration/Repository/IOcelotConfigurationRepository.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Threading.Tasks; -using Ocelot.Configuration.File; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Repository -{ - public interface IOcelotConfigurationRepository - { - Task> Get(); - Task AddOrReplace(IOcelotConfiguration ocelotConfiguration); - } -} \ No newline at end of file diff --git a/src/Ocelot/Configuration/Repository/InMemoryConsulPollerConfiguration.cs b/src/Ocelot/Configuration/Repository/InMemoryConsulPollerConfiguration.cs new file mode 100644 index 00000000..9e411f76 --- /dev/null +++ b/src/Ocelot/Configuration/Repository/InMemoryConsulPollerConfiguration.cs @@ -0,0 +1,7 @@ +namespace Ocelot.Configuration.Repository +{ + public class InMemoryConsulPollerConfiguration : IConsulPollerConfiguration + { + public int Delay => 1000; + } +} diff --git a/src/Ocelot/Configuration/Repository/InMemoryInternalConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/InMemoryInternalConfigurationRepository.cs new file mode 100644 index 00000000..2ac954e3 --- /dev/null +++ b/src/Ocelot/Configuration/Repository/InMemoryInternalConfigurationRepository.cs @@ -0,0 +1,29 @@ +using Ocelot.Responses; + +namespace Ocelot.Configuration.Repository +{ + /// + /// Register as singleton + /// + public class InMemoryInternalConfigurationRepository : IInternalConfigurationRepository + { + private static readonly object LockObject = new object(); + + private IInternalConfiguration _internalConfiguration; + + public Response Get() + { + return new OkResponse(_internalConfiguration); + } + + public Response AddOrReplace(IInternalConfiguration internalConfiguration) + { + lock (LockObject) + { + _internalConfiguration = internalConfiguration; + } + + return new OkResponse(); + } + } +} diff --git a/src/Ocelot/Configuration/Repository/InMemoryOcelotConfigurationRepository.cs b/src/Ocelot/Configuration/Repository/InMemoryOcelotConfigurationRepository.cs deleted file mode 100644 index 9ce50ba6..00000000 --- a/src/Ocelot/Configuration/Repository/InMemoryOcelotConfigurationRepository.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Threading.Tasks; -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 Task> Get() - { - return Task.FromResult>(new OkResponse(_ocelotConfiguration)); - } - - public Task AddOrReplace(IOcelotConfiguration ocelotConfiguration) - { - lock (LockObject) - { - _ocelotConfiguration = ocelotConfiguration; - } - - return Task.FromResult(new OkResponse()); - } - } -} diff --git a/src/Ocelot/Configuration/ServiceProviderConfiguration.cs b/src/Ocelot/Configuration/ServiceProviderConfiguration.cs index aa7c492c..129d24db 100644 --- a/src/Ocelot/Configuration/ServiceProviderConfiguration.cs +++ b/src/Ocelot/Configuration/ServiceProviderConfiguration.cs @@ -2,15 +2,17 @@ { public class ServiceProviderConfiguration { - public ServiceProviderConfiguration(string type, string host, int port) + public ServiceProviderConfiguration(string type, string host, int port, string token) { Host = host; Port = port; + Token = token; Type = type; } - public string Host { get; private set; } - public int Port { get; private set; } - public string Type { get; private set; } + public string Host { get; } + public int Port { get; } + public string Type { get; } + public string Token { get; } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Configuration/Setter/FileConfigurationSetter.cs b/src/Ocelot/Configuration/Setter/FileAndInternalConfigurationSetter.cs similarity index 63% rename from src/Ocelot/Configuration/Setter/FileConfigurationSetter.cs rename to src/Ocelot/Configuration/Setter/FileAndInternalConfigurationSetter.cs index 38a7c1cb..6821553e 100644 --- a/src/Ocelot/Configuration/Setter/FileConfigurationSetter.cs +++ b/src/Ocelot/Configuration/Setter/FileAndInternalConfigurationSetter.cs @@ -1,42 +1,44 @@ -using System.Threading.Tasks; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Repository; -using Ocelot.Responses; - -namespace Ocelot.Configuration.Setter -{ - public class FileConfigurationSetter : IFileConfigurationSetter - { - private readonly IOcelotConfigurationRepository _configRepo; - private readonly IOcelotConfigurationCreator _configCreator; - private readonly IFileConfigurationRepository _repo; - - public FileConfigurationSetter(IOcelotConfigurationRepository configRepo, - IOcelotConfigurationCreator configCreator, IFileConfigurationRepository repo) - { - _configRepo = configRepo; - _configCreator = configCreator; - _repo = repo; - } - - public async Task Set(FileConfiguration fileConfig) - { - var response = await _repo.Set(fileConfig); - - if(response.IsError) - { - return new ErrorResponse(response.Errors); - } - - var config = await _configCreator.Create(fileConfig); - - if(!config.IsError) - { - await _configRepo.AddOrReplace(config.Data); - } - - return new ErrorResponse(config.Errors); - } - } -} +using System.Threading.Tasks; +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; +using Ocelot.Configuration.Repository; +using Ocelot.Responses; + +namespace Ocelot.Configuration.Setter +{ + public class FileAndInternalConfigurationSetter : IFileConfigurationSetter + { + private readonly IInternalConfigurationRepository _configRepo; + private readonly IInternalConfigurationCreator _configCreator; + private readonly IFileConfigurationRepository _repo; + + public FileAndInternalConfigurationSetter( + IInternalConfigurationRepository configRepo, + IInternalConfigurationCreator configCreator, + IFileConfigurationRepository repo) + { + _configRepo = configRepo; + _configCreator = configCreator; + _repo = repo; + } + + public async Task Set(FileConfiguration fileConfig) + { + var response = await _repo.Set(fileConfig); + + if(response.IsError) + { + return new ErrorResponse(response.Errors); + } + + var config = await _configCreator.Create(fileConfig); + + if(!config.IsError) + { + _configRepo.AddOrReplace(config.Data); + } + + return new ErrorResponse(config.Errors); + } + } +} diff --git a/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs b/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs index 4fe3567d..7e06cb6b 100644 --- a/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs +++ b/src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs @@ -17,8 +17,8 @@ namespace Ocelot.Configuration.Validator Errors = errors; } - public bool IsError { get; private set; } + public bool IsError { get; } - public List Errors { get; private set; } + public List Errors { get; } } } diff --git a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs index 56360ad5..9e471475 100644 --- a/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs +++ b/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs @@ -1,21 +1,73 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Memory; - namespace Ocelot.DependencyInjection { + using System; + using System.Collections.Generic; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.Configuration.Memory; + using System.IO; + using System.Linq; + using System.Text.RegularExpressions; + using Configuration.File; + using Newtonsoft.Json; + public static class ConfigurationBuilderExtensions { - [Obsolete("Please set BaseUrl in configuration.json GlobalConfiguration.BaseUrl")] + [Obsolete("Please set BaseUrl in ocelot.json GlobalConfiguration.BaseUrl")] public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder builder, string baseUrl) { - var memorySource = new MemoryConfigurationSource(); - memorySource.InitialData = new List> + var memorySource = new MemoryConfigurationSource { - new KeyValuePair("BaseUrl", baseUrl) + InitialData = new List> + { + new KeyValuePair("BaseUrl", baseUrl) + } }; + builder.Add(memorySource); + + return builder; + } + + public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder) + { + const string pattern = "(?i)ocelot.([a-zA-Z0-9]*).json"; + + var reg = new Regex(pattern); + + var files = Directory.GetFiles(".") + .Where(path => reg.IsMatch(path)) + .ToList(); + + var fileConfiguration = new FileConfiguration(); + + foreach (var file in files) + { + // windows and unix sigh... + if(files.Count > 1 && (file == "./ocelot.json" || file == ".\\ocelot.json")) + { + continue; + } + + var lines = File.ReadAllText(file); + + var config = JsonConvert.DeserializeObject(lines); + + // windows and unix sigh... + if (file == "./ocelot.global.json" || file == ".\\ocelot.global.json") + { + fileConfiguration.GlobalConfiguration = config.GlobalConfiguration; + } + + fileConfiguration.Aggregates.AddRange(config.Aggregates); + fileConfiguration.ReRoutes.AddRange(config.ReRoutes); + } + + var json = JsonConvert.SerializeObject(fileConfiguration); + + File.WriteAllText("ocelot.json", json); + + builder.AddJsonFile("ocelot.json"); + return builder; } } diff --git a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs index 37cfa0d8..7cf514cb 100644 --- a/src/Ocelot/DependencyInjection/IOcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/IOcelotBuilder.cs @@ -3,6 +3,7 @@ using CacheManager.Core; using System; using System.Net.Http; using IdentityServer4.AccessTokenValidation; +using Ocelot.Middleware.Multiplexer; namespace Ocelot.DependencyInjection { @@ -20,8 +21,13 @@ namespace Ocelot.DependencyInjection IOcelotBuilder AddSingletonDelegatingHandler(bool global = false) where T : DelegatingHandler; - + IOcelotBuilder AddTransientDelegatingHandler(bool global = false) where T : DelegatingHandler; + + IOcelotBuilder AddSingletonDefinedAggregator() + where T : class, IDefinedAggregator; + IOcelotBuilder AddTransientDefinedAggregator() + where T : class, IDefinedAggregator; } } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 9e2e763b..672e8540 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -1,7 +1,3 @@ -using Butterfly.Client.Tracing; -using Microsoft.Extensions.Options; -using Ocelot.Middleware.Multiplexer; - namespace Ocelot.DependencyInjection { using CacheManager.Core; @@ -12,17 +8,14 @@ namespace Ocelot.DependencyInjection using Ocelot.Authorisation; using Ocelot.Cache; using Ocelot.Claims; - using Ocelot.Configuration.Authentication; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; - using Ocelot.Configuration.Provider; using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.Configuration.Validator; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.UrlMatcher; - using Ocelot.DownstreamUrlCreator; using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; using Ocelot.Headers; using Ocelot.Infrastructure.Claims.Parser; @@ -44,14 +37,16 @@ namespace Ocelot.DependencyInjection using System.Security.Cryptography.X509Certificates; using IdentityServer4.AccessTokenValidation; using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; using Ocelot.Configuration; - using Ocelot.Configuration.Builder; - using FileConfigurationProvider = Ocelot.Configuration.Provider.FileConfigurationProvider; using Microsoft.Extensions.DependencyInjection.Extensions; - using System.Linq; using System.Net.Http; using Butterfly.Client.AspNetCore; + using Ocelot.Infrastructure; + using Ocelot.Infrastructure.Consul; + using Butterfly.Client.Tracing; + using Ocelot.Middleware.Multiplexer; + using Pivotal.Discovery.Client; + using ServiceDiscovery.Providers; public class OcelotBuilder : IOcelotBuilder { @@ -76,8 +71,8 @@ namespace Ocelot.DependencyInjection _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); @@ -89,9 +84,8 @@ namespace Ocelot.DependencyInjection _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); - _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); @@ -99,7 +93,6 @@ namespace Ocelot.DependencyInjection _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); - _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); _services.TryAddSingleton(); @@ -121,6 +114,17 @@ namespace Ocelot.DependencyInjection _services.TryAddSingleton(); _services.TryAddSingleton(); + if (UsingEurekaServiceDiscoveryProvider(configurationRoot)) + { + _services.AddDiscoveryClient(configurationRoot); + } + else + { + _services.TryAddSingleton(); + } + + _services.TryAddSingleton(); + // 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.TryAddSingleton(); @@ -148,6 +152,12 @@ namespace Ocelot.DependencyInjection // We add this here so that we can always inject something into the factory for IoC.. _services.AddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); + _services.TryAddSingleton(); } public IOcelotAdministrationBuilder AddAdministration(string path, string secret) @@ -182,6 +192,20 @@ namespace Ocelot.DependencyInjection return new OcelotAdministrationBuilder(_services, _configurationRoot); } + public IOcelotBuilder AddSingletonDefinedAggregator() + where T : class, IDefinedAggregator + { + _services.AddSingleton(); + return this; + } + + public IOcelotBuilder AddTransientDefinedAggregator() + where T : class, IDefinedAggregator + { + _services.AddTransient(); + return this; + } + public IOcelotBuilder AddSingletonDelegatingHandler(bool global = false) where THandler : DelegatingHandler { @@ -230,15 +254,6 @@ namespace Ocelot.DependencyInjection public IOcelotBuilder AddStoreOcelotConfigurationInConsul() { - var serviceDiscoveryPort = _configurationRoot.GetValue("GlobalConfiguration:ServiceDiscoveryProvider:Port", 0); - var serviceDiscoveryHost = _configurationRoot.GetValue("GlobalConfiguration:ServiceDiscoveryProvider:Host", string.Empty); - - var config = new ServiceProviderConfigurationBuilder() - .WithServiceDiscoveryProviderPort(serviceDiscoveryPort) - .WithServiceDiscoveryProviderHost(serviceDiscoveryHost) - .Build(); - - _services.AddSingleton(config); _services.AddSingleton(); _services.AddSingleton(); return this; @@ -254,12 +269,12 @@ namespace Ocelot.DependencyInjection _services.AddSingleton>(cacheManagerOutputCache); _services.AddSingleton>(ocelotOutputCacheManager); - var ocelotConfigCacheManagerOutputCache = CacheFactory.Build("OcelotConfigurationCache", settings); - var ocelotConfigCacheManager = new OcelotCacheManagerCache(ocelotConfigCacheManagerOutputCache); - _services.RemoveAll(typeof(ICacheManager)); - _services.RemoveAll(typeof(IOcelotCache)); - _services.AddSingleton>(ocelotConfigCacheManagerOutputCache); - _services.AddSingleton>(ocelotConfigCacheManager); + var ocelotConfigCacheManagerOutputCache = CacheFactory.Build("OcelotConfigurationCache", settings); + var ocelotConfigCacheManager = new OcelotCacheManagerCache(ocelotConfigCacheManagerOutputCache); + _services.RemoveAll(typeof(ICacheManager)); + _services.RemoveAll(typeof(IOcelotCache)); + _services.AddSingleton>(ocelotConfigCacheManagerOutputCache); + _services.AddSingleton>(ocelotConfigCacheManager); var fileConfigCacheManagerOutputCache = CacheFactory.Build("FileConfigurationCache", settings); var fileConfigCacheManager = new OcelotCacheManagerCache(fileConfigCacheManagerOutputCache); @@ -280,7 +295,6 @@ namespace Ocelot.DependencyInjection private void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration, IAdministrationPath adminPath) { _services.TryAddSingleton(identityServerConfiguration); - _services.TryAddSingleton(); var identityServerBuilder = _services .AddIdentityServer(o => { o.IssuerUri = "Ocelot"; @@ -345,5 +359,13 @@ namespace Ocelot.DependencyInjection } }; } + + private static bool UsingEurekaServiceDiscoveryProvider(IConfiguration configurationRoot) + { + var type = configurationRoot.GetValue("GlobalConfiguration:ServiceDiscoveryProvider:Type", + string.Empty); + + return type.ToLower() == "eureka"; + } } } diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs index bcfde4b0..df70b0b3 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs @@ -18,7 +18,7 @@ namespace Ocelot.DownstreamRouteFinder.Finder _placeholderNameAndValueFinder = urlPathPlaceholderNameAndValueFinder; } - public Response FindDownstreamRoute(string path, string httpMethod, IOcelotConfiguration configuration, string upstreamHost) + public Response FindDownstreamRoute(string path, string httpMethod, IInternalConfiguration configuration, string upstreamHost) { var downstreamRoutes = new List(); @@ -44,10 +44,7 @@ namespace Ocelot.DownstreamRouteFinder.Finder return notNullOption != null ? new OkResponse(notNullOption) : new OkResponse(nullOption); } - return new ErrorResponse(new List - { - new UnableToFindDownstreamRouteError() - }); + return new ErrorResponse(new UnableToFindDownstreamRouteError(path, httpMethod)); } private bool RouteIsApplicableToThisRequest(ReRoute reRoute, string httpMethod, string upstreamHost) diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs index 81901231..1d8fae15 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteFinder.cs @@ -6,6 +6,6 @@ namespace Ocelot.DownstreamRouteFinder.Finder { public interface IDownstreamRouteFinder { - Response FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod, IOcelotConfiguration configuration, string upstreamHost); + Response FindDownstreamRoute(string upstreamUrlPath, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost); } } diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs b/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs index 66f7172a..81988ba5 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs @@ -4,7 +4,8 @@ namespace Ocelot.DownstreamRouteFinder.Finder { public class UnableToFindDownstreamRouteError : Error { - public UnableToFindDownstreamRouteError() : base("UnableToFindDownstreamRouteError", OcelotErrorCode.UnableToFindDownstreamRouteError) + public UnableToFindDownstreamRouteError(string path, string httpVerb) + : base($"Unable to find downstream route for path: {path}, verb: {httpVerb}", OcelotErrorCode.UnableToFindDownstreamRouteError) { } } diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index c9da50a4..970636d3 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -using Ocelot.Configuration; -using Ocelot.Configuration.Provider; +using System.Linq; +using Ocelot.Configuration.Repository; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; @@ -13,21 +13,20 @@ namespace Ocelot.DownstreamRouteFinder.Middleware { private readonly OcelotRequestDelegate _next; private readonly IDownstreamRouteFinder _downstreamRouteFinder; - private readonly IOcelotLogger _logger; - private readonly IOcelotConfigurationProvider _configProvider; + private readonly IInternalConfigurationRepository _repo; private readonly IMultiplexer _multiplexer; public DownstreamRouteFinderMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IDownstreamRouteFinder downstreamRouteFinder, - IOcelotConfigurationProvider configProvider, + IInternalConfigurationRepository repo, IMultiplexer multiplexer) + :base(loggerFactory.CreateLogger()) { - _configProvider = configProvider; + _repo = repo; _multiplexer = multiplexer; _next = next; _downstreamRouteFinder = downstreamRouteFinder; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) @@ -36,31 +35,31 @@ namespace Ocelot.DownstreamRouteFinder.Middleware var upstreamHost = context.HttpContext.Request.Headers["Host"]; - var configuration = await _configProvider.Get(); + var configuration = _repo.Get(); if (configuration.IsError) { - _logger.LogError($"{MiddlewareName} setting pipeline errors. IOcelotConfigurationProvider returned {configuration.Errors.ToErrorString()}"); + Logger.LogWarning($"{MiddlewareName} setting pipeline errors. IOcelotConfigurationProvider returned {configuration.Errors.ToErrorString()}"); SetPipelineError(context, configuration.Errors); return; } context.ServiceProviderConfiguration = configuration.Data.ServiceProviderConfiguration; - _logger.LogDebug("upstream url path is {upstreamUrlPath}", upstreamUrlPath); + Logger.LogDebug($"Upstream url path is {upstreamUrlPath}"); var downstreamRoute = _downstreamRouteFinder.FindDownstreamRoute(upstreamUrlPath, context.HttpContext.Request.Method, configuration.Data, upstreamHost); if (downstreamRoute.IsError) { - _logger.LogError($"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {downstreamRoute.Errors.ToErrorString()}"); + Logger.LogWarning($"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {downstreamRoute.Errors.ToErrorString()}"); SetPipelineError(context, downstreamRoute.Errors); return; - } - - // todo - put this back in - //// _logger.LogDebug("downstream template is {downstreamRoute.Data.ReRoute.DownstreamPath}", downstreamRoute.Data.ReRoute.DownstreamReRoute.DownstreamPathTemplate); + } + + var downstreamPathTemplates = string.Join(", ", downstreamRoute.Data.ReRoute.DownstreamReRoute.Select(r => r.DownstreamPathTemplate.Value)); + Logger.LogDebug($"downstream templates are {downstreamPathTemplates}"); context.TemplatePlaceholderNameAndValues = downstreamRoute.Data.TemplatePlaceholderNameAndValues; diff --git a/src/Ocelot/DownstreamUrlCreator/DownstreamHostNullOrEmptyError.cs b/src/Ocelot/DownstreamUrlCreator/DownstreamHostNullOrEmptyError.cs deleted file mode 100644 index d56532a0..00000000 --- a/src/Ocelot/DownstreamUrlCreator/DownstreamHostNullOrEmptyError.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 69528d43..00000000 --- a/src/Ocelot/DownstreamUrlCreator/DownstreamPathNullOrEmptyError.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 9f83bfee..00000000 --- a/src/Ocelot/DownstreamUrlCreator/DownstreamSchemeNullOrEmptyError.cs +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 9975cec2..00000000 --- a/src/Ocelot/DownstreamUrlCreator/IUrlBuilder.cs +++ /dev/null @@ -1,12 +0,0 @@ -/* -using Ocelot.Responses; -using Ocelot.Values; - -namespace Ocelot.DownstreamUrlCreator -{ - public interface IUrlBuilder - { - Response Build(string downstreamPath, string downstreamScheme, ServiceHostAndPort downstreamHostAndPort); - } -} -*/ diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs index 308daea5..bc054783 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -16,15 +16,14 @@ namespace Ocelot.DownstreamUrlCreator.Middleware { private readonly OcelotRequestDelegate _next; private readonly IDownstreamPathPlaceholderReplacer _replacer; - private readonly IOcelotLogger _logger; public DownstreamUrlCreatorMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IDownstreamPathPlaceholderReplacer replacer) + :base(loggerFactory.CreateLogger()) { _next = next; _replacer = replacer; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) @@ -34,55 +33,42 @@ namespace Ocelot.DownstreamUrlCreator.Middleware if (dsPath.IsError) { - _logger.LogDebug("IDownstreamPathPlaceholderReplacer returned an error, setting pipeline error"); + Logger.LogDebug("IDownstreamPathPlaceholderReplacer returned an error, setting pipeline error"); SetPipelineError(context, dsPath.Errors); return; } - UriBuilder uriBuilder; - + context.DownstreamRequest.Scheme = context.DownstreamReRoute.DownstreamScheme; + if (ServiceFabricRequest(context)) { - uriBuilder = CreateServiceFabricUri(context, dsPath); + var pathAndQuery = CreateServiceFabricUri(context, dsPath); + context.DownstreamRequest.AbsolutePath = pathAndQuery.path; + context.DownstreamRequest.Query = pathAndQuery.query; } else { - uriBuilder = new UriBuilder(context.DownstreamRequest.RequestUri) - { - Path = dsPath.Data.Value, - Scheme = context.DownstreamReRoute.DownstreamScheme - }; + context.DownstreamRequest.AbsolutePath = dsPath.Data.Value; } - context.DownstreamRequest.RequestUri = uriBuilder.Uri; - - _logger.LogDebug("downstream url is {downstreamUrl.Data.Value}", context.DownstreamRequest.RequestUri); + Logger.LogDebug($"Downstream url is {context.DownstreamRequest}"); await _next.Invoke(context); } - private UriBuilder CreateServiceFabricUri(DownstreamContext context, Response dsPath) + private (string path, string query) CreateServiceFabricUri(DownstreamContext context, Response dsPath) { - var query = context.DownstreamRequest.RequestUri.Query; - var scheme = context.DownstreamReRoute.DownstreamScheme; - var host = context.DownstreamRequest.RequestUri.Host; - var port = context.DownstreamRequest.RequestUri.Port; + var query = context.DownstreamRequest.Query; var serviceFabricPath = $"/{context.DownstreamReRoute.ServiceName + dsPath.Data.Value}"; - Uri uri; - if (RequestForStatefullService(query)) { - uri = new Uri($"{scheme}://{host}:{port}{serviceFabricPath}{query}"); - } - else - { - var split = string.IsNullOrEmpty(query) ? "?" : "&"; - uri = new Uri($"{scheme}://{host}:{port}{serviceFabricPath}{query}{split}cmd=instance"); + return (serviceFabricPath, query); } - return new UriBuilder(uri); + var split = string.IsNullOrEmpty(query) ? "?" : "&"; + return (serviceFabricPath, $"{query}{split}cmd=instance"); } private static bool ServiceFabricRequest(DownstreamContext context) diff --git a/src/Ocelot/DownstreamUrlCreator/UrlBuilder.cs b/src/Ocelot/DownstreamUrlCreator/UrlBuilder.cs deleted file mode 100644 index 1c5f284c..00000000 --- a/src/Ocelot/DownstreamUrlCreator/UrlBuilder.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* -using System; -using System.Collections.Generic; -using Ocelot.Errors; -using Ocelot.Responses; -using Ocelot.Values; - -namespace Ocelot.DownstreamUrlCreator -{ - public class UrlBuilder : IUrlBuilder - { - public Response Build(string downstreamPath, string downstreamScheme, ServiceHostAndPort 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)); - } - } -} -*/ diff --git a/src/Ocelot/Errors/Error.cs b/src/Ocelot/Errors/Error.cs index 520f3775..e063934b 100644 --- a/src/Ocelot/Errors/Error.cs +++ b/src/Ocelot/Errors/Error.cs @@ -16,4 +16,4 @@ namespace Ocelot.Errors return Message; } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index 2b9e22e2..41b0b83d 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -1,10 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; -using Ocelot.Configuration.Provider; -using Ocelot.DownstreamRouteFinder.Middleware; +using Ocelot.Configuration.Repository; using Ocelot.Infrastructure.Extensions; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; @@ -18,68 +15,64 @@ namespace Ocelot.Errors.Middleware public class ExceptionHandlerMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; - private readonly IOcelotLogger _logger; - private readonly IOcelotConfigurationProvider _provider; + private readonly IInternalConfigurationRepository _configRepo; private readonly IRequestScopedDataRepository _repo; public ExceptionHandlerMiddleware(OcelotRequestDelegate next, - IOcelotLoggerFactory loggerFactory, - IOcelotConfigurationProvider provider, + IOcelotLoggerFactory loggerFactory, + IInternalConfigurationRepository configRepo, IRequestScopedDataRepository repo) + : base(loggerFactory.CreateLogger()) { - _provider = provider; + _configRepo = configRepo; _repo = repo; _next = next; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) { try { - await TrySetGlobalRequestId(context); + TrySetGlobalRequestId(context); - _logger.LogDebug("ocelot pipeline started"); + Logger.LogDebug("ocelot pipeline started"); await _next.Invoke(context); } catch (Exception e) { - _logger.LogDebug("error calling middleware"); + Logger.LogDebug("error calling middleware"); var message = CreateMessage(context, e); - _logger.LogError(message, e); + Logger.LogError(message, e); SetInternalServerErrorOnResponse(context); } - _logger.LogDebug("ocelot pipeline finished"); + Logger.LogDebug("ocelot pipeline finished"); } - private async Task TrySetGlobalRequestId(DownstreamContext context) + private void TrySetGlobalRequestId(DownstreamContext context) { - //try and get the global request id and set it for logs... - //should this basically be immutable per request...i guess it should! - //first thing is get config - var configuration = await _provider.Get(); + //try and get the global request id and set it for logs... + //should this basically be immutable per request...i guess it should! + //first thing is get config + var configuration = _configRepo.Get(); - //if error throw to catch below.. - if(configuration.IsError) - { - throw new Exception($"{MiddlewareName} setting pipeline errors. IOcelotConfigurationProvider returned {configuration.Errors.ToErrorString()}"); - } - - //else set the request id? - var key = configuration.Data.RequestId; - - StringValues upstreamRequestIds; - if (!string.IsNullOrEmpty(key) && context.HttpContext.Request.Headers.TryGetValue(key, out upstreamRequestIds)) - { - //todo fix looking in both places - context.HttpContext.TraceIdentifier = upstreamRequestIds.First(); - _repo.Add("RequestId", context.HttpContext.TraceIdentifier); + if(configuration.IsError) + { + throw new Exception($"{MiddlewareName} setting pipeline errors. IOcelotConfigurationProvider returned {configuration.Errors.ToErrorString()}"); } + + var key = configuration.Data.RequestId; + + if (!string.IsNullOrEmpty(key) && context.HttpContext.Request.Headers.TryGetValue(key, out var upstreamRequestIds)) + { + context.HttpContext.TraceIdentifier = upstreamRequestIds.First(); + } + + _repo.Add("RequestId", context.HttpContext.TraceIdentifier); } private void SetInternalServerErrorOnResponse(DownstreamContext context) diff --git a/src/Ocelot/Errors/OcelotErrorCode.cs b/src/Ocelot/Errors/OcelotErrorCode.cs index b976e29c..8109bb52 100644 --- a/src/Ocelot/Errors/OcelotErrorCode.cs +++ b/src/Ocelot/Errors/OcelotErrorCode.cs @@ -34,6 +34,8 @@ RateLimitOptionsError, PathTemplateDoesntStartWithForwardSlash, FileValidationFailedError, - UnableToFindDelegatingHandlerProviderError + UnableToFindDelegatingHandlerProviderError, + CouldNotFindPlaceholderError, + CouldNotFindAggregatorError } } diff --git a/src/Ocelot/Headers/AddHeadersToRequest.cs b/src/Ocelot/Headers/AddHeadersToRequest.cs index c8ec8cf3..a7b5b689 100644 --- a/src/Ocelot/Headers/AddHeadersToRequest.cs +++ b/src/Ocelot/Headers/AddHeadersToRequest.cs @@ -4,6 +4,9 @@ using Ocelot.Configuration; using Ocelot.Infrastructure.Claims.Parser; using Ocelot.Responses; using System.Net.Http; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration.Creator; +using Ocelot.Request.Middleware; namespace Ocelot.Headers { @@ -16,7 +19,7 @@ namespace Ocelot.Headers _claimsParser = claimsParser; } - public Response SetHeadersOnDownstreamRequest(List claimsToThings, IEnumerable claims, HttpRequestMessage downstreamRequest) + public Response SetHeadersOnDownstreamRequest(List claimsToThings, IEnumerable claims, DownstreamRequest downstreamRequest) { foreach (var config in claimsToThings) { @@ -39,5 +42,19 @@ namespace Ocelot.Headers return new OkResponse(); } + + public void SetHeadersOnDownstreamRequest(IEnumerable headers, HttpContext context) + { + var requestHeader = context.Request.Headers; + foreach (var header in headers) + { + if (requestHeader.ContainsKey(header.Key)) + { + requestHeader.Remove(header.Key); + } + + requestHeader.Add(header.Key, header.Value); + } + } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Headers/AddHeadersToResponse.cs b/src/Ocelot/Headers/AddHeadersToResponse.cs new file mode 100644 index 00000000..6cdf9a22 --- /dev/null +++ b/src/Ocelot/Headers/AddHeadersToResponse.cs @@ -0,0 +1,43 @@ +namespace Ocelot.Headers +{ + using System.Collections.Generic; + using Ocelot.Configuration.Creator; + using Ocelot.Infrastructure; + using Ocelot.Logging; + using Ocelot.Middleware; + + public class AddHeadersToResponse : IAddHeadersToResponse + { + private readonly IPlaceholders _placeholders; + private readonly IOcelotLogger _logger; + + public AddHeadersToResponse(IPlaceholders placeholders, IOcelotLoggerFactory factory) + { + _logger = factory.CreateLogger(); + _placeholders = placeholders; + } + + public void Add(List addHeaders, DownstreamResponse response) + { + foreach(var add in addHeaders) + { + if(add.Value.StartsWith('{') && add.Value.EndsWith('}')) + { + var value = _placeholders.Get(add.Value); + + if(value.IsError) + { + _logger.LogWarning($"Unable to add header to response {add.Key}: {add.Value}"); + continue; + } + + response.Headers.Add(new Header(add.Key, new List { value.Data })); + } + else + { + response.Headers.Add(new Header(add.Key, new List { add.Value })); + } + } + } + } +} diff --git a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs index cb45ffc3..2bc43cfe 100644 --- a/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs +++ b/src/Ocelot/Headers/HttpResponseHeaderReplacer.cs @@ -1,53 +1,50 @@ -using System; using System.Collections.Generic; using System.Linq; -using System.Net.Http; using Ocelot.Configuration; +using Ocelot.Infrastructure; using Ocelot.Infrastructure.Extensions; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Request.Middleware; using Ocelot.Responses; namespace Ocelot.Headers { public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer { - private Dictionary> _placeholders; + private readonly IPlaceholders _placeholders; - public HttpResponseHeaderReplacer() + public HttpResponseHeaderReplacer(IPlaceholders placeholders) { - _placeholders = new Dictionary>(); - _placeholders.Add("{DownstreamBaseUrl}", x => { - var downstreamUrl = $"{x.RequestUri.Scheme}://{x.RequestUri.Host}"; - - if(x.RequestUri.Port != 80 && x.RequestUri.Port != 443) - { - downstreamUrl = $"{downstreamUrl}:{x.RequestUri.Port}"; - } - - return $"{downstreamUrl}/"; - }); + _placeholders = placeholders; } - public Response Replace(HttpResponseMessage response, List fAndRs, HttpRequestMessage httpRequestMessage) + public Response Replace(DownstreamResponse response, List fAndRs, DownstreamRequest request) { foreach (var f in fAndRs) { + var dict = response.Headers.ToDictionary(x => x.Key); + //if the response headers contain a matching find and replace - if(response.Headers.TryGetValues(f.Key, out var values)) + if(dict.TryGetValue(f.Key, out var values)) { //check to see if it is a placeholder in the find... - if(_placeholders.TryGetValue(f.Find, out var replacePlaceholder)) + var placeholderValue = _placeholders.Get(f.Find, request); + + if(!placeholderValue.IsError) { //if it is we need to get the value of the placeholder - var find = replacePlaceholder(httpRequestMessage); - var replaced = values.ToList()[f.Index].Replace(find, f.Replace.LastCharAsForwardSlash()); - response.Headers.Remove(f.Key); - response.Headers.Add(f.Key, replaced); + var replaced = values.Values.ToList()[f.Index].Replace(placeholderValue.Data, f.Replace.LastCharAsForwardSlash()); + + response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); + response.Headers.Add(new Header(f.Key, new List { replaced })); } else { - var replaced = values.ToList()[f.Index].Replace(f.Find, f.Replace); - response.Headers.Remove(f.Key); - response.Headers.Add(f.Key, replaced); + var replaced = values.Values.ToList()[f.Index].Replace(f.Find, f.Replace); + + response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); + response.Headers.Add(new Header(f.Key, new List { replaced })); } } } diff --git a/src/Ocelot/Headers/IAddHeadersToRequest.cs b/src/Ocelot/Headers/IAddHeadersToRequest.cs index 387b3f01..a0afb0c0 100644 --- a/src/Ocelot/Headers/IAddHeadersToRequest.cs +++ b/src/Ocelot/Headers/IAddHeadersToRequest.cs @@ -1,13 +1,19 @@ -namespace Ocelot.Headers +using Microsoft.AspNetCore.Http; + +namespace Ocelot.Headers { using System.Collections.Generic; using System.Net.Http; using Ocelot.Configuration; + using Ocelot.Configuration.Creator; + using Ocelot.Infrastructure.RequestData; + using Ocelot.Request.Middleware; using Ocelot.Responses; public interface IAddHeadersToRequest { - Response SetHeadersOnDownstreamRequest(List claimsToThings, IEnumerable claims, HttpRequestMessage downstreamRequest); + Response SetHeadersOnDownstreamRequest(List claimsToThings, IEnumerable claims, DownstreamRequest downstreamRequest); + void SetHeadersOnDownstreamRequest(IEnumerable headers, HttpContext context); } } diff --git a/src/Ocelot/Headers/IAddHeadersToResponse.cs b/src/Ocelot/Headers/IAddHeadersToResponse.cs new file mode 100644 index 00000000..210ddd32 --- /dev/null +++ b/src/Ocelot/Headers/IAddHeadersToResponse.cs @@ -0,0 +1,13 @@ +using Ocelot.Middleware; + +namespace Ocelot.Headers +{ + using System.Collections.Generic; + using Ocelot.Configuration.Creator; + using Ocelot.Middleware.Multiplexer; + + public interface IAddHeadersToResponse + { + void Add(List addHeaders, DownstreamResponse response); + } +} diff --git a/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs b/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs index 8e74d111..b871ac19 100644 --- a/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs +++ b/src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs @@ -1,12 +1,14 @@ using System.Collections.Generic; -using System.Net.Http; using Ocelot.Configuration; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Request.Middleware; using Ocelot.Responses; namespace Ocelot.Headers { public interface IHttpResponseHeaderReplacer { - Response Replace(HttpResponseMessage response, List fAndRs, HttpRequestMessage httpRequestMessage); + Response Replace(DownstreamResponse response, List fAndRs, DownstreamRequest httpRequestMessage); } -} \ No newline at end of file +} diff --git a/src/Ocelot/Headers/IRemoveOutputHeaders.cs b/src/Ocelot/Headers/IRemoveOutputHeaders.cs index 339d0c35..8c081c3b 100644 --- a/src/Ocelot/Headers/IRemoveOutputHeaders.cs +++ b/src/Ocelot/Headers/IRemoveOutputHeaders.cs @@ -1,10 +1,12 @@ -using System.Net.Http.Headers; +using System.Collections.Generic; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; using Ocelot.Responses; namespace Ocelot.Headers { public interface IRemoveOutputHeaders { - Response Remove(HttpResponseHeaders headers); + Response Remove(List
headers); } } diff --git a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs index f4281a23..9dfe3d64 100644 --- a/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs @@ -1,7 +1,4 @@ using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Ocelot.DownstreamRouteFinder.Middleware; -using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; @@ -10,19 +7,24 @@ namespace Ocelot.Headers.Middleware public class HttpHeadersTransformationMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; - private readonly IOcelotLogger _logger; private readonly IHttpContextRequestHeaderReplacer _preReplacer; private readonly IHttpResponseHeaderReplacer _postReplacer; + private readonly IAddHeadersToResponse _addHeadersToResponse; + private readonly IAddHeadersToRequest _addHeadersToRequest; public HttpHeadersTransformationMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpContextRequestHeaderReplacer preReplacer, - IHttpResponseHeaderReplacer postReplacer) + IHttpResponseHeaderReplacer postReplacer, + IAddHeadersToResponse addHeadersToResponse, + IAddHeadersToRequest addHeadersToRequest) + :base(loggerFactory.CreateLogger()) { + _addHeadersToResponse = addHeadersToResponse; + _addHeadersToRequest = addHeadersToRequest; _next = next; _postReplacer = postReplacer; _preReplacer = preReplacer; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) @@ -32,11 +34,15 @@ namespace Ocelot.Headers.Middleware //todo - this should be on httprequestmessage not httpcontext? _preReplacer.Replace(context.HttpContext, preFAndRs); + _addHeadersToRequest.SetHeadersOnDownstreamRequest(context.DownstreamReRoute.AddHeadersToUpstream, context.HttpContext); + await _next.Invoke(context); var postFAndRs = context.DownstreamReRoute.DownstreamHeadersFindAndReplace; _postReplacer.Replace(context.DownstreamResponse, postFAndRs, context.DownstreamRequest); + + _addHeadersToResponse.Add(context.DownstreamReRoute.AddHeadersToDownstream, context.DownstreamResponse); } } } diff --git a/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs b/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs index 6e8dc31e..ba340b3a 100644 --- a/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/HttpRequestHeadersBuilderMiddleware.cs @@ -12,34 +12,33 @@ namespace Ocelot.Headers.Middleware { private readonly OcelotRequestDelegate _next; private readonly IAddHeadersToRequest _addHeadersToRequest; - private readonly IOcelotLogger _logger; public HttpRequestHeadersBuilderMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, - IAddHeadersToRequest addHeadersToRequest) + IAddHeadersToRequest addHeadersToRequest) + :base(loggerFactory.CreateLogger()) { _next = next; _addHeadersToRequest = addHeadersToRequest; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) { if (context.DownstreamReRoute.ClaimsToHeaders.Any()) { - _logger.LogDebug($"{ context.DownstreamReRoute.DownstreamPathTemplate.Value} has instructions to convert claims to headers"); + Logger.LogInformation($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} has instructions to convert claims to headers"); var response = _addHeadersToRequest.SetHeadersOnDownstreamRequest(context.DownstreamReRoute.ClaimsToHeaders, context.HttpContext.User.Claims, context.DownstreamRequest); if (response.IsError) { - _logger.LogDebug("Error setting headers on context, setting pipeline error"); + Logger.LogWarning("Error setting headers on context, setting pipeline error"); SetPipelineError(context, response.Errors); return; } - _logger.LogDebug("headers have been set on context"); + Logger.LogInformation("headers have been set on context"); } await _next.Invoke(context); diff --git a/src/Ocelot/Headers/RemoveOutputHeaders.cs b/src/Ocelot/Headers/RemoveOutputHeaders.cs index 19d863eb..2664b696 100644 --- a/src/Ocelot/Headers/RemoveOutputHeaders.cs +++ b/src/Ocelot/Headers/RemoveOutputHeaders.cs @@ -1,4 +1,7 @@ -using System.Net.Http.Headers; +using System.Collections.Generic; +using System.Linq; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; using Ocelot.Responses; namespace Ocelot.Headers @@ -14,14 +17,11 @@ namespace Ocelot.Headers { "Transfer-Encoding" }; - public Response Remove(HttpResponseHeaders headers) - { - foreach (var unsupported in _unsupportedRequestHeaders) - { - headers.Remove(unsupported); - } + public Response Remove(List
headers) + { + headers.RemoveAll(x => _unsupportedRequestHeaders.Contains(x.Key)); return new OkResponse(); } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs b/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs index 1af0acbc..773b4375 100644 --- a/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs +++ b/src/Ocelot/Infrastructure/Claims/Parser/ClaimsParser.cs @@ -26,10 +26,7 @@ if (splits.Length < index || index < 0) { - return new ErrorResponse(new List - { - new CannotFindClaimError($"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}") - }); + return new ErrorResponse(new CannotFindClaimError($"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}")); } var value = splits[index]; @@ -55,10 +52,7 @@ return new OkResponse(claim.Value); } - return new ErrorResponse(new List - { - new CannotFindClaimError($"Cannot find claim for key: {key}") - }); + return new ErrorResponse(new CannotFindClaimError($"Cannot find claim for key: {key}")); } } } diff --git a/src/Ocelot/Infrastructure/Consul/ConsulClientFactory.cs b/src/Ocelot/Infrastructure/Consul/ConsulClientFactory.cs new file mode 100644 index 00000000..26799934 --- /dev/null +++ b/src/Ocelot/Infrastructure/Consul/ConsulClientFactory.cs @@ -0,0 +1,22 @@ +using System; +using Consul; +using Ocelot.ServiceDiscovery.Configuration; + +namespace Ocelot.Infrastructure.Consul +{ + public class ConsulClientFactory : IConsulClientFactory + { + public IConsulClient Get(ConsulRegistryConfiguration config) + { + return new ConsulClient(c => + { + c.Address = new Uri($"http://{config.Host}:{config.Port}"); + + if (!string.IsNullOrEmpty(config?.Token)) + { + c.Token = config.Token; + } + }); + } + } +} diff --git a/src/Ocelot/Infrastructure/Consul/IConsulClientFactory.cs b/src/Ocelot/Infrastructure/Consul/IConsulClientFactory.cs new file mode 100644 index 00000000..43428686 --- /dev/null +++ b/src/Ocelot/Infrastructure/Consul/IConsulClientFactory.cs @@ -0,0 +1,10 @@ +using Consul; +using Ocelot.ServiceDiscovery.Configuration; + +namespace Ocelot.Infrastructure.Consul +{ + public interface IConsulClientFactory + { + IConsulClient Get(ConsulRegistryConfiguration config); + } +} diff --git a/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs b/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs new file mode 100644 index 00000000..c6214e83 --- /dev/null +++ b/src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Infrastructure +{ + public class CouldNotFindPlaceholderError : Error + { + public CouldNotFindPlaceholderError(string placeholder) + : base($"Unable to find placeholder called {placeholder}", OcelotErrorCode.CouldNotFindPlaceholderError) + { + } + } +} \ No newline at end of file diff --git a/src/Ocelot/Infrastructure/Extensions/StringValuesExtensions.cs b/src/Ocelot/Infrastructure/Extensions/StringValuesExtensions.cs new file mode 100644 index 00000000..3c85ce5c --- /dev/null +++ b/src/Ocelot/Infrastructure/Extensions/StringValuesExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Primitives; +using System.Linq; + +namespace Ocelot.Infrastructure.Extensions +{ + internal static class StringValuesExtensions + { + public static string GetValue(this StringValues stringValues) + { + if (stringValues.Count == 1) + { + return stringValues; + } + + return stringValues.ToArray().LastOrDefault(); + } + } +} diff --git a/src/Ocelot/Infrastructure/IPlaceholders.cs b/src/Ocelot/Infrastructure/IPlaceholders.cs new file mode 100644 index 00000000..1d2bbfa5 --- /dev/null +++ b/src/Ocelot/Infrastructure/IPlaceholders.cs @@ -0,0 +1,12 @@ +using System.Net.Http; +using Ocelot.Request.Middleware; +using Ocelot.Responses; + +namespace Ocelot.Infrastructure +{ + public interface IPlaceholders + { + Response Get(string key); + Response Get(string key, DownstreamRequest request); + } +} \ No newline at end of file diff --git a/src/Ocelot/Infrastructure/Placeholders.cs b/src/Ocelot/Infrastructure/Placeholders.cs new file mode 100644 index 00000000..c43dea14 --- /dev/null +++ b/src/Ocelot/Infrastructure/Placeholders.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; +using Ocelot.Responses; + +namespace Ocelot.Infrastructure +{ + public class Placeholders : IPlaceholders + { + private readonly Dictionary>> _placeholders; + private readonly Dictionary> _requestPlaceholders; + private readonly IBaseUrlFinder _finder; + private readonly IRequestScopedDataRepository _repo; + + public Placeholders(IBaseUrlFinder finder, IRequestScopedDataRepository repo) + { + _repo = repo; + _finder = finder; + _placeholders = new Dictionary>>(); + _placeholders.Add("{BaseUrl}", () => new OkResponse(_finder.Find())); + _placeholders.Add("{TraceId}", () => { + var traceId = _repo.Get("TraceId"); + if(traceId.IsError) + { + return new ErrorResponse(traceId.Errors); + } + + return new OkResponse(traceId.Data); + }); + + _requestPlaceholders = new Dictionary>(); + _requestPlaceholders.Add("{DownstreamBaseUrl}", x => { + var downstreamUrl = $"{x.Scheme}://{x.Host}"; + + if(x.Port != 80 && x.Port != 443) + { + downstreamUrl = $"{downstreamUrl}:{x.Port}"; + } + + return $"{downstreamUrl}/"; + }); + } + + public Response Get(string key) + { + if(_placeholders.ContainsKey(key)) + { + var response = _placeholders[key].Invoke(); + if(!response.IsError) + { + return new OkResponse(response.Data); + } + } + + return new ErrorResponse(new CouldNotFindPlaceholderError(key)); + } + + public Response Get(string key, DownstreamRequest request) + { + if(_requestPlaceholders.ContainsKey(key)) + { + return new OkResponse(_requestPlaceholders[key].Invoke(request)); + } + + return new ErrorResponse(new CouldNotFindPlaceholderError(key)); + } + } +} diff --git a/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs b/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs index 0f75411d..ec2ce4e0 100644 --- a/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs +++ b/src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs @@ -24,10 +24,7 @@ namespace Ocelot.Infrastructure.RequestData } catch (Exception exception) { - return new ErrorResponse(new List - { - new CannotAddDataError(string.Format($"Unable to add data for key: {key}, exception: {exception.Message}")) - }); + return new ErrorResponse(new CannotAddDataError(string.Format($"Unable to add data for key: {key}, exception: {exception.Message}"))); } } @@ -40,10 +37,7 @@ namespace Ocelot.Infrastructure.RequestData } catch (Exception exception) { - return new ErrorResponse(new List - { - new CannotAddDataError(string.Format($"Unable to update data for key: {key}, exception: {exception.Message}")) - }); + return new ErrorResponse(new CannotAddDataError(string.Format($"Unable to update data for key: {key}, exception: {exception.Message}"))); } } @@ -53,10 +47,7 @@ namespace Ocelot.Infrastructure.RequestData if(_httpContextAccessor.HttpContext == null || _httpContextAccessor.HttpContext.Items == null) { - return new ErrorResponse(new List - { - new CannotFindDataError($"Unable to find data for key: {key} because HttpContext or HttpContext.Items is null") - }); + return new ErrorResponse(new CannotFindDataError($"Unable to find data for key: {key} because HttpContext or HttpContext.Items is null")); } if(_httpContextAccessor.HttpContext.Items.TryGetValue(key, out obj)) @@ -65,10 +56,7 @@ namespace Ocelot.Infrastructure.RequestData return new OkResponse(data); } - return new ErrorResponse(new List - { - new CannotFindDataError($"Unable to find data for key: {key}") - }); + return new ErrorResponse(new CannotFindDataError($"Unable to find data for key: {key}")); } } } diff --git a/test/Ocelot.UnitTests/Wait.cs b/src/Ocelot/Infrastructure/Wait.cs similarity index 82% rename from test/Ocelot.UnitTests/Wait.cs rename to src/Ocelot/Infrastructure/Wait.cs index d8fd3a88..934e31e5 100644 --- a/test/Ocelot.UnitTests/Wait.cs +++ b/src/Ocelot/Infrastructure/Wait.cs @@ -1,4 +1,4 @@ -namespace Ocelot.UnitTests +namespace Ocelot.Infrastructure { public class Wait { @@ -7,4 +7,4 @@ namespace Ocelot.UnitTests return new Waiter(milliSeconds); } } -} \ No newline at end of file +} diff --git a/test/Ocelot.UnitTests/Waiter.cs b/src/Ocelot/Infrastructure/Waiter.cs similarity index 93% rename from test/Ocelot.UnitTests/Waiter.cs rename to src/Ocelot/Infrastructure/Waiter.cs index 8847c5b9..730bdbff 100644 --- a/test/Ocelot.UnitTests/Waiter.cs +++ b/src/Ocelot/Infrastructure/Waiter.cs @@ -1,47 +1,47 @@ -using System; -using System.Diagnostics; - -namespace Ocelot.UnitTests -{ - public class Waiter - { - private readonly int _milliSeconds; - - public Waiter(int milliSeconds) - { - _milliSeconds = milliSeconds; - } - - public bool Until(Func condition) - { - var stopwatch = Stopwatch.StartNew(); - var passed = false; - while (stopwatch.ElapsedMilliseconds < _milliSeconds) - { - if (condition.Invoke()) - { - passed = true; - break; - } - } - - return passed; - } - - public bool Until(Func condition) - { - var stopwatch = Stopwatch.StartNew(); - var passed = false; - while (stopwatch.ElapsedMilliseconds < _milliSeconds) - { - if (condition.Invoke()) - { - passed = true; - break; - } - } - - return passed; - } - } -} +using System; +using System.Diagnostics; + +namespace Ocelot.Infrastructure +{ + public class Waiter + { + private readonly int _milliSeconds; + + public Waiter(int milliSeconds) + { + _milliSeconds = milliSeconds; + } + + public bool Until(Func condition) + { + var stopwatch = Stopwatch.StartNew(); + var passed = false; + while (stopwatch.ElapsedMilliseconds < _milliSeconds) + { + if (condition.Invoke()) + { + passed = true; + break; + } + } + + return passed; + } + + public bool Until(Func condition) + { + var stopwatch = Stopwatch.StartNew(); + var passed = false; + while (stopwatch.ElapsedMilliseconds < _milliSeconds) + { + if (condition.Invoke()) + { + passed = true; + break; + } + } + + return passed; + } + } +} diff --git a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs index 1c99c3c5..2810e021 100644 --- a/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs +++ b/src/Ocelot/LoadBalancer/LoadBalancers/LeastConnection.cs @@ -28,12 +28,12 @@ namespace Ocelot.LoadBalancer.LoadBalancers if (services == null) { - return new ErrorResponse(new List() { new ServicesAreNullError($"services were null for {_serviceName}") }); + return new ErrorResponse(new ServicesAreNullError($"services were null for {_serviceName}") ); } if (!services.Any()) { - return new ErrorResponse(new List() { new ServicesAreEmptyError($"services were empty for {_serviceName}") }); + return new ErrorResponse(new ServicesAreEmptyError($"services were empty for {_serviceName}")); } lock(_syncLock) diff --git a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs index 8c2e963a..f5ddf6f2 100644 --- a/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs +++ b/src/Ocelot/LoadBalancer/Middleware/LoadBalancingMiddleware.cs @@ -1,27 +1,22 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Ocelot.DownstreamRouteFinder.Middleware; -using Ocelot.Infrastructure.RequestData; using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Logging; using Ocelot.Middleware; -using Ocelot.QueryStrings.Middleware; namespace Ocelot.LoadBalancer.Middleware { public class LoadBalancingMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; - private readonly IOcelotLogger _logger; private readonly ILoadBalancerHouse _loadBalancerHouse; public LoadBalancingMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, ILoadBalancerHouse loadBalancerHouse) + :base(loggerFactory.CreateLogger()) { _next = next; - _logger = loggerFactory.CreateLogger(); _loadBalancerHouse = loadBalancerHouse; } @@ -30,7 +25,7 @@ namespace Ocelot.LoadBalancer.Middleware var loadBalancer = await _loadBalancerHouse.Get(context.DownstreamReRoute, context.ServiceProviderConfiguration); if(loadBalancer.IsError) { - _logger.LogDebug("there was an error retriving the loadbalancer, setting pipeline error"); + Logger.LogDebug("there was an error retriving the loadbalancer, setting pipeline error"); SetPipelineError(context, loadBalancer.Errors); return; } @@ -38,29 +33,25 @@ namespace Ocelot.LoadBalancer.Middleware var hostAndPort = await loadBalancer.Data.Lease(); if(hostAndPort.IsError) { - _logger.LogDebug("there was an error leasing the loadbalancer, setting pipeline error"); + Logger.LogDebug("there was an error leasing the loadbalancer, setting pipeline error"); SetPipelineError(context, hostAndPort.Errors); return; } - var uriBuilder = new UriBuilder(context.DownstreamRequest.RequestUri); - - uriBuilder.Host = hostAndPort.Data.DownstreamHost; + context.DownstreamRequest.Host = hostAndPort.Data.DownstreamHost; if (hostAndPort.Data.DownstreamPort > 0) { - uriBuilder.Port = hostAndPort.Data.DownstreamPort; + context.DownstreamRequest.Port = hostAndPort.Data.DownstreamPort; } - context.DownstreamRequest.RequestUri = uriBuilder.Uri; - try { await _next.Invoke(context); } catch (Exception) { - _logger.LogDebug("Exception calling next middleware, exception will be thrown to global handler"); + Logger.LogDebug("Exception calling next middleware, exception will be thrown to global handler"); throw; } finally diff --git a/src/Ocelot/Logging/AspDotNetLogger.cs b/src/Ocelot/Logging/AspDotNetLogger.cs index eae1eec5..5ee04e13 100644 --- a/src/Ocelot/Logging/AspDotNetLogger.cs +++ b/src/Ocelot/Logging/AspDotNetLogger.cs @@ -1,6 +1,5 @@ using System; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Internal; using Ocelot.Infrastructure.RequestData; namespace Ocelot.Logging @@ -10,34 +9,38 @@ namespace Ocelot.Logging private readonly ILogger _logger; private readonly IRequestScopedDataRepository _scopedDataRepository; - public string Name { get; } - - public AspDotNetLogger(ILogger logger, IRequestScopedDataRepository scopedDataRepository, string typeName) + public AspDotNetLogger(ILogger logger, IRequestScopedDataRepository scopedDataRepository) { - Name = typeName; _logger = logger; _scopedDataRepository = scopedDataRepository; } - public void LogTrace(string message, params object[] args) + public void LogTrace(string message) { var requestId = GetOcelotRequestId(); var previousRequestId = GetOcelotPreviousRequestId(); - _logger.LogTrace("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, new FormattedLogValues(message, args).ToString()); + _logger.LogTrace("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}", requestId, previousRequestId, message); } - public void LogDebug(string message, params object[] args) + public void LogDebug(string message) { var requestId = GetOcelotRequestId(); var previousRequestId = GetOcelotPreviousRequestId(); - _logger.LogDebug("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, new FormattedLogValues(message, args).ToString()); + _logger.LogDebug("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}", requestId, previousRequestId, message); } - public void LogInformation(string message, params object[] args) + public void LogInformation(string message) { var requestId = GetOcelotRequestId(); var previousRequestId = GetOcelotPreviousRequestId(); - _logger.LogInformation("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message},", requestId, previousRequestId, new FormattedLogValues(message, args).ToString()); + _logger.LogInformation("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}", requestId, previousRequestId, message); + } + + public void LogWarning(string message) + { + var requestId = GetOcelotRequestId(); + var previousRequestId = GetOcelotPreviousRequestId(); + _logger.LogWarning("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}", requestId, previousRequestId, message); } public void LogError(string message, Exception exception) @@ -47,18 +50,11 @@ namespace Ocelot.Logging _logger.LogError("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}, exception: {exception}", requestId, previousRequestId, message, exception); } - public void LogError(string message, params object[] args) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - _logger.LogError("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}", requestId, previousRequestId, new FormattedLogValues(message, args).ToString()); - } - public void LogCritical(string message, Exception exception) { var requestId = GetOcelotRequestId(); var previousRequestId = GetOcelotPreviousRequestId(); - _logger.LogError("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}", requestId, previousRequestId, message); + _logger.LogCritical("requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}, exception: {exception}", requestId, previousRequestId, message, exception); } private string GetOcelotRequestId() @@ -85,4 +81,4 @@ namespace Ocelot.Logging return requestId.Data; } } -} \ No newline at end of file +} diff --git a/src/Ocelot/Logging/AspDotNetLoggerFactory.cs b/src/Ocelot/Logging/AspDotNetLoggerFactory.cs index 298653bc..b988e09c 100644 --- a/src/Ocelot/Logging/AspDotNetLoggerFactory.cs +++ b/src/Ocelot/Logging/AspDotNetLoggerFactory.cs @@ -1,8 +1,8 @@ -using Microsoft.Extensions.Logging; -using Ocelot.Infrastructure.RequestData; - -namespace Ocelot.Logging -{ +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; + +namespace Ocelot.Logging +{ public class AspDotNetLoggerFactory : IOcelotLoggerFactory { private readonly ILoggerFactory _loggerFactory; @@ -17,7 +17,7 @@ namespace Ocelot.Logging public IOcelotLogger CreateLogger() { var logger = _loggerFactory.CreateLogger(); - return new AspDotNetLogger(logger, _scopedDataRepository, typeof(T).Name); + return new AspDotNetLogger(logger, _scopedDataRepository); } - } + } } \ No newline at end of file diff --git a/src/Ocelot/Logging/IOcelotLogger.cs b/src/Ocelot/Logging/IOcelotLogger.cs new file mode 100644 index 00000000..7d1c1205 --- /dev/null +++ b/src/Ocelot/Logging/IOcelotLogger.cs @@ -0,0 +1,17 @@ +using System; + +namespace Ocelot.Logging +{ + /// + /// Thin wrapper around the DotNet core logging framework, used to allow the scopedDataRepository to be injected giving access to the Ocelot RequestId + /// + public interface IOcelotLogger + { + void LogTrace(string message); + void LogDebug(string message); + void LogInformation(string message); + void LogWarning(string message); + void LogError(string message, Exception exception); + void LogCritical(string message, Exception exception); + } +} diff --git a/src/Ocelot/Logging/IOcelotLoggerFactory.cs b/src/Ocelot/Logging/IOcelotLoggerFactory.cs index 3a145595..2407afcd 100644 --- a/src/Ocelot/Logging/IOcelotLoggerFactory.cs +++ b/src/Ocelot/Logging/IOcelotLoggerFactory.cs @@ -1,27 +1,7 @@ -using System; - -namespace Ocelot.Logging +namespace Ocelot.Logging { public interface IOcelotLoggerFactory { IOcelotLogger CreateLogger(); } - - /// - /// Thin wrapper around the DotNet core logging framework, used to allow the scopedDataRepository to be injected giving access to the Ocelot RequestId - /// - public interface IOcelotLogger - { - void LogTrace(string message, params object[] args); - void LogDebug(string message, params object[] args); - void LogInformation(string message, params object[] args); - void LogError(string message, Exception exception); - void LogError(string message, params object[] args); - void LogCritical(string message, Exception exception); - - /// - /// The name of the type the logger has been built for. - /// - string Name { get; } - } } diff --git a/src/Ocelot/Logging/OcelotDiagnosticListener.cs b/src/Ocelot/Logging/OcelotDiagnosticListener.cs index ee0b5b8a..f2c51760 100644 --- a/src/Ocelot/Logging/OcelotDiagnosticListener.cs +++ b/src/Ocelot/Logging/OcelotDiagnosticListener.cs @@ -3,18 +3,47 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DiagnosticAdapter; using Butterfly.Client.AspNetCore; using Butterfly.OpenTracing; +using Ocelot.Middleware; +using Butterfly.Client.Tracing; +using System.Linq; +using System.Collections.Generic; +using Ocelot.Infrastructure.Extensions; +using Ocelot.Requester; namespace Ocelot.Logging { public class OcelotDiagnosticListener { - private IOcelotLogger _logger; + private readonly IServiceTracer _tracer; + private readonly IOcelotLogger _logger; - public OcelotDiagnosticListener(IOcelotLoggerFactory factory) + public OcelotDiagnosticListener(IOcelotLoggerFactory factory, IServiceTracer tracer) { + _tracer = tracer; _logger = factory.CreateLogger(); } + [DiagnosticName("Ocelot.MiddlewareException")] + public virtual void OcelotMiddlewareException(Exception exception, DownstreamContext context, string name) + { + _logger.LogTrace($"Ocelot.MiddlewareException: {name}; {exception.Message};"); + Event(context.HttpContext, $"Ocelot.MiddlewareStarted: {name}; {context.HttpContext.Request.Path}"); + } + + [DiagnosticName("Ocelot.MiddlewareStarted")] + public virtual void OcelotMiddlewareStarted(DownstreamContext context, string name) + { + _logger.LogTrace($"Ocelot.MiddlewareStarted: {name}; {context.HttpContext.Request.Path}"); + Event(context.HttpContext, $"Ocelot.MiddlewareStarted: {name}; {context.HttpContext.Request.Path}"); + } + + [DiagnosticName("Ocelot.MiddlewareFinished")] + public virtual void OcelotMiddlewareFinished(DownstreamContext context, string name) + { + _logger.LogTrace($"Ocelot.MiddlewareFinished: {name}; {context.HttpContext.Request.Path}"); + Event(context.HttpContext, $"OcelotMiddlewareFinished: {name}; {context.HttpContext.Request.Path}"); + } + [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting")] public virtual void OnMiddlewareStarting(HttpContext httpContext, string name) { @@ -25,7 +54,7 @@ namespace Ocelot.Logging [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException")] public virtual void OnMiddlewareException(Exception exception, string name) { - _logger.LogTrace($"MiddlewareException: {name}; {exception.Message}"); + _logger.LogTrace($"MiddlewareException: {name}; {exception.Message};"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareFinished")] @@ -36,8 +65,31 @@ namespace Ocelot.Logging } private void Event(HttpContext httpContext, string @event) - { + { + // todo - if the user isnt using tracing the code gets here and will blow up on + // _tracer.Tracer.TryExtract. We already use the fake tracer for another scenario + // so sticking it here as well..I guess we need a factory for this but cba to do it at + // the moment + if(_tracer.GetType() == typeof(FakeServiceTracer)) + { + return; + } + var span = httpContext.GetSpan(); + + if(span == null) + { + var spanBuilder = new SpanBuilder($"server {httpContext.Request.Method} {httpContext.Request.Path}"); + if (_tracer.Tracer.TryExtract(out var spanContext, httpContext.Request.Headers, (c, k) => c[k].GetValue(), + c => c.Select(x => new KeyValuePair(x.Key, x.Value.GetValue())).GetEnumerator())) + { + spanBuilder.AsChildOf(spanContext); + } + + span = _tracer.Start(spanBuilder); + httpContext.SetSpan(span); + } + span?.Log(LogField.CreateNew().Event(@event)); } } diff --git a/src/Ocelot/Middleware/DownstreamContext.cs b/src/Ocelot/Middleware/DownstreamContext.cs index 1c805deb..1913eea9 100644 --- a/src/Ocelot/Middleware/DownstreamContext.cs +++ b/src/Ocelot/Middleware/DownstreamContext.cs @@ -1,9 +1,12 @@ +using System; using System.Collections.Generic; using System.Net.Http; using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Errors; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Request.Middleware; namespace Ocelot.Middleware { @@ -11,17 +14,24 @@ namespace Ocelot.Middleware { public DownstreamContext(HttpContext httpContext) { - this.HttpContext = httpContext; + HttpContext = httpContext; Errors = new List(); } public List TemplatePlaceholderNameAndValues { get; set; } + public ServiceProviderConfiguration ServiceProviderConfiguration {get; set;} - public HttpContext HttpContext { get; private set; } + + public HttpContext HttpContext { get; } + public DownstreamReRoute DownstreamReRoute { get; set; } - public HttpRequestMessage DownstreamRequest { get; set; } - public HttpResponseMessage DownstreamResponse { get; set; } - public List Errors { get;set; } + + public DownstreamRequest DownstreamRequest { get; set; } + + public DownstreamResponse DownstreamResponse { get; set; } + + public List Errors { get; } + public bool IsError => Errors.Count > 0; } } diff --git a/src/Ocelot/Middleware/DownstreamResponse.cs b/src/Ocelot/Middleware/DownstreamResponse.cs new file mode 100644 index 00000000..19345735 --- /dev/null +++ b/src/Ocelot/Middleware/DownstreamResponse.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; + +namespace Ocelot.Middleware +{ + public class DownstreamResponse + { + public DownstreamResponse(HttpContent content, HttpStatusCode statusCode, List
headers) + { + Content = content; + StatusCode = statusCode; + Headers = headers ?? new List
(); + } + + public DownstreamResponse(HttpResponseMessage response) + :this(response.Content, response.StatusCode, response.Headers.Select(x => new Header(x.Key, x.Value)).ToList()) + { + } + + public DownstreamResponse(HttpContent content, HttpStatusCode statusCode, IEnumerable>> headers) + :this(content, statusCode, headers.Select(x => new Header(x.Key, x.Value)).ToList()) + { + } + + public HttpContent Content { get; } + public HttpStatusCode StatusCode { get; } + public List
Headers { get; } + } +} diff --git a/src/Ocelot/Middleware/Header.cs b/src/Ocelot/Middleware/Header.cs new file mode 100644 index 00000000..96065699 --- /dev/null +++ b/src/Ocelot/Middleware/Header.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Ocelot.Middleware +{ + public class Header + { + public Header(string key, IEnumerable values) + { + Key = key; + Values = values ?? new List(); + } + + public string Key { get; } + public IEnumerable Values { get; } + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/CouldNotFindAggregatorError.cs b/src/Ocelot/Middleware/Multiplexer/CouldNotFindAggregatorError.cs new file mode 100644 index 00000000..76a19d7c --- /dev/null +++ b/src/Ocelot/Middleware/Multiplexer/CouldNotFindAggregatorError.cs @@ -0,0 +1,12 @@ +using Ocelot.Errors; + +namespace Ocelot.Middleware.Multiplexer +{ + public class CouldNotFindAggregatorError : Error + { + public CouldNotFindAggregatorError(string aggregator) + : base($"Could not find Aggregator: {aggregator}", OcelotErrorCode.CouldNotFindAggregatorError) + { + } + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/IDefinedAggregator.cs b/src/Ocelot/Middleware/Multiplexer/IDefinedAggregator.cs new file mode 100644 index 00000000..b1585165 --- /dev/null +++ b/src/Ocelot/Middleware/Multiplexer/IDefinedAggregator.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Ocelot.Middleware.Multiplexer +{ + public interface IDefinedAggregator + { + Task Aggregate(List responses); + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/IDefinedAggregatorProvider.cs b/src/Ocelot/Middleware/Multiplexer/IDefinedAggregatorProvider.cs new file mode 100644 index 00000000..375ebc33 --- /dev/null +++ b/src/Ocelot/Middleware/Multiplexer/IDefinedAggregatorProvider.cs @@ -0,0 +1,10 @@ +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Middleware.Multiplexer +{ + public interface IDefinedAggregatorProvider + { + Response Get(ReRoute reRoute); + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/IResponseAggregator.cs b/src/Ocelot/Middleware/Multiplexer/IResponseAggregator.cs index b85c2cc8..89f2b00b 100644 --- a/src/Ocelot/Middleware/Multiplexer/IResponseAggregator.cs +++ b/src/Ocelot/Middleware/Multiplexer/IResponseAggregator.cs @@ -6,6 +6,6 @@ namespace Ocelot.Middleware.Multiplexer { public interface IResponseAggregator { - Task Aggregate(ReRoute reRoute, DownstreamContext originalContext, List downstreamContexts); + Task Aggregate(ReRoute reRoute, DownstreamContext originalContext, List downstreamResponses); } } diff --git a/src/Ocelot/Middleware/Multiplexer/IResponseAggregatorFactory.cs b/src/Ocelot/Middleware/Multiplexer/IResponseAggregatorFactory.cs new file mode 100644 index 00000000..b032ff1f --- /dev/null +++ b/src/Ocelot/Middleware/Multiplexer/IResponseAggregatorFactory.cs @@ -0,0 +1,9 @@ +using Ocelot.Configuration; + +namespace Ocelot.Middleware.Multiplexer +{ + public interface IResponseAggregatorFactory + { + IResponseAggregator Get(ReRoute reRoute); + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/InMemoryResponseAggregatorFactory.cs b/src/Ocelot/Middleware/Multiplexer/InMemoryResponseAggregatorFactory.cs new file mode 100644 index 00000000..a74386db --- /dev/null +++ b/src/Ocelot/Middleware/Multiplexer/InMemoryResponseAggregatorFactory.cs @@ -0,0 +1,26 @@ +using Ocelot.Configuration; + +namespace Ocelot.Middleware.Multiplexer +{ + public class InMemoryResponseAggregatorFactory : IResponseAggregatorFactory + { + private readonly UserDefinedResponseAggregator _userDefined; + private readonly SimpleJsonResponseAggregator _simple; + + public InMemoryResponseAggregatorFactory(IDefinedAggregatorProvider provider) + { + _userDefined = new UserDefinedResponseAggregator(provider); + _simple = new SimpleJsonResponseAggregator(); + } + + public IResponseAggregator Get(ReRoute reRoute) + { + if(!string.IsNullOrEmpty(reRoute.Aggregator)) + { + return _userDefined; + } + + return _simple; + } + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/Multiplexer.cs b/src/Ocelot/Middleware/Multiplexer/Multiplexer.cs index c4ebb08b..ffe530e8 100644 --- a/src/Ocelot/Middleware/Multiplexer/Multiplexer.cs +++ b/src/Ocelot/Middleware/Multiplexer/Multiplexer.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Ocelot.Configuration; @@ -6,11 +7,11 @@ namespace Ocelot.Middleware.Multiplexer { public class Multiplexer : IMultiplexer { - private readonly IResponseAggregator _aggregator; + private readonly IResponseAggregatorFactory _factory; - public Multiplexer(IResponseAggregator aggregator) + public Multiplexer(IResponseAggregatorFactory factory) { - _aggregator = aggregator; + _factory = factory; } public async Task Multiplex(DownstreamContext context, ReRoute reRoute, OcelotRequestDelegate next) @@ -31,15 +32,40 @@ namespace Ocelot.Middleware.Multiplexer await Task.WhenAll(tasks); - var downstreamContexts = new List(); + var contexts = new List(); foreach (var task in tasks) { var finished = await task; - downstreamContexts.Add(finished); + contexts.Add(finished); } - await _aggregator.Aggregate(reRoute, context, downstreamContexts); + await Map(reRoute, context, contexts); + } + + private async Task Map(ReRoute reRoute, DownstreamContext context, List contexts) + { + if (reRoute.DownstreamReRoute.Count > 1) + { + var aggregator = _factory.Get(reRoute); + await aggregator.Aggregate(reRoute, context, contexts); + } + else + { + MapNotAggregate(context, contexts); + } + } + + private void MapNotAggregate(DownstreamContext originalContext, List downstreamContexts) + { + //assume at least one..if this errors then it will be caught by global exception handler + var finished = downstreamContexts.First(); + + originalContext.Errors.AddRange(finished.Errors); + + originalContext.DownstreamRequest = finished.DownstreamRequest; + + originalContext.DownstreamResponse = finished.DownstreamResponse; } private async Task Fire(DownstreamContext context, OcelotRequestDelegate next) diff --git a/src/Ocelot/Middleware/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs b/src/Ocelot/Middleware/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs new file mode 100644 index 00000000..b7b85d26 --- /dev/null +++ b/src/Ocelot/Middleware/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Responses; + +namespace Ocelot.Middleware.Multiplexer +{ + public class ServiceLocatorDefinedAggregatorProvider : IDefinedAggregatorProvider + { + private readonly Dictionary _aggregators; + + public ServiceLocatorDefinedAggregatorProvider(IServiceProvider services) + { + _aggregators = services.GetServices().ToDictionary(x => x.GetType().Name); + } + + public Response Get(ReRoute reRoute) + { + if(_aggregators.ContainsKey(reRoute.Aggregator)) + { + return new OkResponse(_aggregators[reRoute.Aggregator]); + } + + return new ErrorResponse(new CouldNotFindAggregatorError(reRoute.Aggregator)); + } + } +} diff --git a/src/Ocelot/Middleware/Multiplexer/SimpleJsonResponseAggregator.cs b/src/Ocelot/Middleware/Multiplexer/SimpleJsonResponseAggregator.cs index 3d9a997d..bfc1351f 100644 --- a/src/Ocelot/Middleware/Multiplexer/SimpleJsonResponseAggregator.cs +++ b/src/Ocelot/Middleware/Multiplexer/SimpleJsonResponseAggregator.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; @@ -12,18 +11,6 @@ namespace Ocelot.Middleware.Multiplexer public class SimpleJsonResponseAggregator : IResponseAggregator { public async Task Aggregate(ReRoute reRoute, DownstreamContext originalContext, List downstreamContexts) - { - if (reRoute.DownstreamReRoute.Count > 1) - { - await MapAggregtes(originalContext, downstreamContexts); - } - else - { - MapNotAggregate(originalContext, downstreamContexts); - } - } - - private async Task MapAggregtes(DownstreamContext originalContext, List downstreamContexts) { await MapAggregateContent(originalContext, downstreamContexts); } @@ -34,7 +21,7 @@ namespace Ocelot.Middleware.Multiplexer contentBuilder.Append("{"); - for (int i = 0; i < downstreamContexts.Count; i++) + for (var i = 0; i < downstreamContexts.Count; i++) { if (downstreamContexts[i].IsError) { @@ -54,13 +41,12 @@ namespace Ocelot.Middleware.Multiplexer contentBuilder.Append("}"); - originalContext.DownstreamResponse = new HttpResponseMessage(HttpStatusCode.OK) + var stringContent = new StringContent(contentBuilder.ToString()) { - Content = new StringContent(contentBuilder.ToString()) - { - Headers = {ContentType = new MediaTypeHeaderValue("application/json")} - } + Headers = {ContentType = new MediaTypeHeaderValue("application/json")} }; + + originalContext.DownstreamResponse = new DownstreamResponse(stringContent, HttpStatusCode.OK, new List>>()); } private static void MapAggregateError(DownstreamContext originalContext, List downstreamContexts, int i) @@ -68,17 +54,5 @@ namespace Ocelot.Middleware.Multiplexer originalContext.Errors.AddRange(downstreamContexts[i].Errors); originalContext.DownstreamResponse = downstreamContexts[i].DownstreamResponse; } - - private void MapNotAggregate(DownstreamContext originalContext, List downstreamContexts) - { - //assume at least one..if this errors then it will be caught by global exception handler - var finished = downstreamContexts.First(); - - originalContext.Errors = finished.Errors; - - originalContext.DownstreamRequest = finished.DownstreamRequest; - - originalContext.DownstreamResponse = finished.DownstreamResponse; - } } } diff --git a/src/Ocelot/Middleware/Multiplexer/UserDefinedResponseAggregator.cs b/src/Ocelot/Middleware/Multiplexer/UserDefinedResponseAggregator.cs new file mode 100644 index 00000000..aa44466d --- /dev/null +++ b/src/Ocelot/Middleware/Multiplexer/UserDefinedResponseAggregator.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ocelot.Configuration; + +namespace Ocelot.Middleware.Multiplexer +{ + public class UserDefinedResponseAggregator : IResponseAggregator + { + private readonly IDefinedAggregatorProvider _provider; + + public UserDefinedResponseAggregator(IDefinedAggregatorProvider provider) + { + _provider = provider; + } + + public async Task Aggregate(ReRoute reRoute, DownstreamContext originalContext, List downstreamResponses) + { + var aggregator = _provider.Get(reRoute); + + if (!aggregator.IsError) + { + var aggregateResponse = await aggregator.Data + .Aggregate(downstreamResponses.Select(x => x.DownstreamResponse).ToList()); + + originalContext.DownstreamResponse = aggregateResponse; + } + else + { + originalContext.Errors.AddRange(aggregator.Errors); + } + } + } +} diff --git a/src/Ocelot/Middleware/OcelotMiddleware.cs b/src/Ocelot/Middleware/OcelotMiddleware.cs index bf49fd03..fe51fd24 100644 --- a/src/Ocelot/Middleware/OcelotMiddleware.cs +++ b/src/Ocelot/Middleware/OcelotMiddleware.cs @@ -1,20 +1,33 @@ using System.Collections.Generic; using Ocelot.Errors; +using Ocelot.Logging; namespace Ocelot.Middleware { public abstract class OcelotMiddleware - { - protected OcelotMiddleware() + { + protected OcelotMiddleware(IOcelotLogger logger) { + Logger = logger; MiddlewareName = this.GetType().Name; } + public IOcelotLogger Logger { get; } + public string MiddlewareName { get; } public void SetPipelineError(DownstreamContext context, List errors) { - context.Errors = errors; + foreach(var error in errors) + { + SetPipelineError(context, error); + } + } + + public void SetPipelineError(DownstreamContext context, Error error) + { + Logger.LogWarning(error.Message); + context.Errors.Add(error); } } } diff --git a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs index cda82bef..4fc3db7a 100644 --- a/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs +++ b/src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs @@ -4,14 +4,12 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using System.Diagnostics; using Microsoft.AspNetCore.Builder; using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; - using Ocelot.Configuration.Provider; using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.Responses; @@ -19,14 +17,10 @@ using Rafty.Concensus; using Rafty.Infrastructure; using Ocelot.Middleware.Pipeline; + using Pivotal.Discovery.Client; public static class OcelotMiddlewareExtensions { - /// - /// Registers the Ocelot default middlewares - /// - /// - /// public static async Task UseOcelot(this IApplicationBuilder builder) { await builder.UseOcelot(new OcelotPipelineConfiguration()); @@ -34,12 +28,6 @@ return builder; } - /// - /// Registers Ocelot with a combination of default middlewares and optional middlewares in the configuration - /// - /// - /// - /// public static async Task UseOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration) { var configuration = await CreateConfiguration(builder); @@ -51,6 +39,11 @@ SetUpRafty(builder); } + if (UsingEurekaServiceDiscoveryProvider(configuration)) + { + builder.UseDiscoveryClient(); + } + ConfigureDiagnosticListener(builder); var pipelineBuilder = new OcelotPipelineBuilder(builder.ApplicationServices); @@ -65,6 +58,8 @@ rest of asp.net.. */ + builder.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware"; + builder.Use(async (context, task) => { var downstreamContext = new DownstreamContext(context); @@ -74,6 +69,11 @@ return builder; } + private static bool UsingEurekaServiceDiscoveryProvider(IInternalConfiguration configuration) + { + return configuration?.ServiceProviderConfiguration != null && configuration.ServiceProviderConfiguration.Type?.ToLower() == "eureka"; + } + private static bool UsingRafty(IApplicationBuilder builder) { var possible = builder.ApplicationServices.GetService(typeof(INode)) as INode; @@ -94,63 +94,108 @@ node.Start(nodeId.Id); } - private static async Task CreateConfiguration(IApplicationBuilder builder) + private static async Task CreateConfiguration(IApplicationBuilder builder) { - var deps = GetDependencies(builder); + // make configuration from file system? + // earlier user needed to add ocelot files in startup configuration stuff, asp.net will map it to this + var fileConfig = (IOptions)builder.ApplicationServices.GetService(typeof(IOptions)); + + // now create the config + var internalConfigCreator = (IInternalConfigurationCreator)builder.ApplicationServices.GetService(typeof(IInternalConfigurationCreator)); + var internalConfig = await internalConfigCreator.Create(fileConfig.Value); - var ocelotConfiguration = await deps.provider.Get(); + // now save it in memory + var internalConfigRepo = (IInternalConfigurationRepository)builder.ApplicationServices.GetService(typeof(IInternalConfigurationRepository)); + internalConfigRepo.AddOrReplace(internalConfig.Data); - if (ConfigurationNotSetUp(ocelotConfiguration)) + var fileConfigSetter = (IFileConfigurationSetter)builder.ApplicationServices.GetService(typeof(IFileConfigurationSetter)); + + var fileConfigRepo = (IFileConfigurationRepository)builder.ApplicationServices.GetService(typeof(IFileConfigurationRepository)); + + if (UsingConsul(fileConfigRepo)) { - var response = await SetConfig(builder, deps.fileConfiguration, deps.setter, deps.provider, deps.repo); - - if (UnableToSetConfig(response)) + await SetFileConfigInConsul(builder, fileConfigRepo, fileConfig, internalConfigCreator, internalConfigRepo); + } + else + { + await SetFileConfig(fileConfigSetter, fileConfig); + } + + return GetOcelotConfigAndReturn(internalConfigRepo); + } + + private static async Task SetFileConfigInConsul(IApplicationBuilder builder, + IFileConfigurationRepository fileConfigRepo, IOptions fileConfig, + IInternalConfigurationCreator internalConfigCreator, IInternalConfigurationRepository internalConfigRepo) + { + // get the config from consul. + var fileConfigFromConsul = await fileConfigRepo.Get(); + + if (IsError(fileConfigFromConsul)) + { + ThrowToStopOcelotStarting(fileConfigFromConsul); + } + else if (ConfigNotStoredInConsul(fileConfigFromConsul)) + { + //there was no config in consul set the file in config in consul + await fileConfigRepo.Set(fileConfig.Value); + } + else + { + // create the internal config from consul data + var internalConfig = await internalConfigCreator.Create(fileConfigFromConsul.Data); + + if (IsError(internalConfig)) { - ThrowToStopOcelotStarting(response); + ThrowToStopOcelotStarting(internalConfig); + } + else + { + // add the internal config to the internal repo + var response = internalConfigRepo.AddOrReplace(internalConfig.Data); + + if (IsError(response)) + { + ThrowToStopOcelotStarting(response); + } + } + + if (IsError(internalConfig)) + { + ThrowToStopOcelotStarting(internalConfig); } } - return await GetOcelotConfigAndReturn(deps.provider); + //todo - this starts the poller if it has been registered...please this is so bad. + var hack = builder.ApplicationServices.GetService(typeof(ConsulFileConfigurationPoller)); } - private static async Task SetConfig(IApplicationBuilder builder, IOptions fileConfiguration, IFileConfigurationSetter setter, IOcelotConfigurationProvider provider, IFileConfigurationRepository repo) + private static async Task SetFileConfig(IFileConfigurationSetter fileConfigSetter, IOptions fileConfig) { - if (UsingConsul(repo)) + Response response; + response = await fileConfigSetter.Set(fileConfig.Value); + + if (IsError(response)) { - return await SetUpConfigFromConsul(builder, repo, setter, fileConfiguration); + ThrowToStopOcelotStarting(response); } - - return await setter.Set(fileConfiguration.Value); } - private static bool UnableToSetConfig(Response response) + private static bool ConfigNotStoredInConsul(Responses.Response fileConfigFromConsul) + { + return fileConfigFromConsul.Data == null; + } + + private static bool IsError(Response response) { return response == null || response.IsError; } - private static bool ConfigurationNotSetUp(Ocelot.Responses.Response ocelotConfiguration) + private static IInternalConfiguration GetOcelotConfigAndReturn(IInternalConfigurationRepository provider) { - return ocelotConfiguration == null || ocelotConfiguration.Data == null || ocelotConfiguration.IsError; - } + var ocelotConfiguration = provider.Get(); - private static (IOptions fileConfiguration, IFileConfigurationSetter setter, IOcelotConfigurationProvider provider, IFileConfigurationRepository repo) GetDependencies(IApplicationBuilder builder) - { - var fileConfiguration = (IOptions)builder.ApplicationServices.GetService(typeof(IOptions)); - - var setter = (IFileConfigurationSetter)builder.ApplicationServices.GetService(typeof(IFileConfigurationSetter)); - - var provider = (IOcelotConfigurationProvider)builder.ApplicationServices.GetService(typeof(IOcelotConfigurationProvider)); - - var repo = (IFileConfigurationRepository)builder.ApplicationServices.GetService(typeof(IFileConfigurationRepository)); - - return (fileConfiguration, setter, provider, repo); - } - - private static async Task GetOcelotConfigAndReturn(IOcelotConfigurationProvider provider) - { - var ocelotConfiguration = await provider.Get(); - - if(ocelotConfiguration == null || ocelotConfiguration.Data == null || ocelotConfiguration.IsError) + if(ocelotConfiguration?.Data == null || ocelotConfiguration.IsError) { ThrowToStopOcelotStarting(ocelotConfiguration); } @@ -168,49 +213,7 @@ return fileConfigRepo.GetType() == typeof(ConsulFileConfigurationRepository); } - private static async Task SetUpConfigFromConsul(IApplicationBuilder builder, IFileConfigurationRepository consulFileConfigRepo, IFileConfigurationSetter setter, IOptions fileConfig) - { - Response config = null; - - var ocelotConfigurationRepository = - (IOcelotConfigurationRepository) builder.ApplicationServices.GetService( - typeof(IOcelotConfigurationRepository)); - - var ocelotConfigurationCreator = - (IOcelotConfigurationCreator) builder.ApplicationServices.GetService( - typeof(IOcelotConfigurationCreator)); - - var fileConfigFromConsul = await consulFileConfigRepo.Get(); - - if (fileConfigFromConsul.Data == null) - { - config = await setter.Set(fileConfig.Value); - var hack = builder.ApplicationServices.GetService(typeof(ConsulFileConfigurationPoller)); - } - else - { - var ocelotConfig = await ocelotConfigurationCreator.Create(fileConfigFromConsul.Data); - - if(ocelotConfig.IsError) - { - return new ErrorResponse(ocelotConfig.Errors); - } - - config = await ocelotConfigurationRepository.AddOrReplace(ocelotConfig.Data); - - if (config.IsError) - { - return new ErrorResponse(config.Errors); - } - - //todo - this starts the poller if it has been registered...please this is so bad. - var hack = builder.ApplicationServices.GetService(typeof(ConsulFileConfigurationPoller)); - } - - return new OkResponse(); - } - - private static void CreateAdministrationArea(IApplicationBuilder builder, IOcelotConfiguration configuration) + private static void CreateAdministrationArea(IApplicationBuilder builder, IInternalConfiguration configuration) { if(!string.IsNullOrEmpty(configuration.AdministrationPath)) { @@ -228,26 +231,14 @@ }); } } - - private static void UseIfNotNull(this IApplicationBuilder builder, Func, Task> middleware) - { - if (middleware != null) - { - builder.Use(middleware); - } - } - /// - /// Configure a DiagnosticListener to listen for diagnostic events when the middleware starts and ends - /// - /// - private static void ConfigureDiagnosticListener(IApplicationBuilder builder) - { + private static void ConfigureDiagnosticListener(IApplicationBuilder builder) + { var env = (IHostingEnvironment)builder.ApplicationServices.GetService(typeof(IHostingEnvironment)); var listener = (OcelotDiagnosticListener)builder.ApplicationServices.GetService(typeof(OcelotDiagnosticListener)); var diagnosticListener = (DiagnosticListener)builder.ApplicationServices.GetService(typeof(DiagnosticListener)); diagnosticListener.SubscribeWithAdapter(listener); - } + } private static void OnShutdown(IApplicationBuilder app) { diff --git a/src/Ocelot/Middleware/Pipeline/IOcelotPipelineBuilder.cs b/src/Ocelot/Middleware/Pipeline/IOcelotPipelineBuilder.cs index 3bc0d6b0..9cb0db56 100644 --- a/src/Ocelot/Middleware/Pipeline/IOcelotPipelineBuilder.cs +++ b/src/Ocelot/Middleware/Pipeline/IOcelotPipelineBuilder.cs @@ -11,5 +11,6 @@ namespace Ocelot.Middleware.Pipeline IServiceProvider ApplicationServices { get; } OcelotPipelineBuilder Use(Func middleware); OcelotRequestDelegate Build(); + IOcelotPipelineBuilder New(); } } diff --git a/src/Ocelot/Middleware/Pipeline/MapWhenMiddleware.cs b/src/Ocelot/Middleware/Pipeline/MapWhenMiddleware.cs new file mode 100644 index 00000000..f05c35e4 --- /dev/null +++ b/src/Ocelot/Middleware/Pipeline/MapWhenMiddleware.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading.Tasks; + +namespace Ocelot.Middleware.Pipeline +{ + public class MapWhenMiddleware + { + private readonly OcelotRequestDelegate _next; + private readonly MapWhenOptions _options; + + public MapWhenMiddleware(OcelotRequestDelegate next, MapWhenOptions options) + { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _next = next; + _options = options; + } + + public async Task Invoke(DownstreamContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (_options.Predicate(context)) + { + await _options.Branch(context); + } + else + { + await _next(context); + } + } + } +} diff --git a/src/Ocelot/Middleware/Pipeline/MapWhenOptions.cs b/src/Ocelot/Middleware/Pipeline/MapWhenOptions.cs new file mode 100644 index 00000000..912688c3 --- /dev/null +++ b/src/Ocelot/Middleware/Pipeline/MapWhenOptions.cs @@ -0,0 +1,28 @@ +using System; + +namespace Ocelot.Middleware.Pipeline +{ + public class MapWhenOptions + { + private Func _predicate; + + public Func Predicate + { + get + { + return _predicate; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _predicate = value; + } + } + + public OcelotRequestDelegate Branch { get; set; } + } +} diff --git a/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilder.cs b/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilder.cs index 1e37514c..5877ab62 100644 --- a/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilder.cs +++ b/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilder.cs @@ -19,6 +19,12 @@ namespace Ocelot.Middleware.Pipeline _middlewares = new List>(); } + public OcelotPipelineBuilder(IOcelotPipelineBuilder builder) + { + ApplicationServices = builder.ApplicationServices; + _middlewares = new List>(); + } + public IServiceProvider ApplicationServices { get; } public OcelotPipelineBuilder Use(Func middleware) @@ -42,5 +48,10 @@ namespace Ocelot.Middleware.Pipeline return app; } + + public IOcelotPipelineBuilder New() + { + return new OcelotPipelineBuilder(this); + } } } diff --git a/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilderExtensions.cs b/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilderExtensions.cs index 422097fc..b9aabfe3 100644 --- a/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilderExtensions.cs +++ b/src/Ocelot/Middleware/Pipeline/OcelotPipelineBuilderExtensions.cs @@ -3,6 +3,7 @@ // Removed code and changed RequestDelete to OcelotRequestDelete, HttpContext to DownstreamContext, removed some exception handling messages using System; +using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -12,6 +13,8 @@ using Microsoft.Extensions.DependencyInjection; namespace Ocelot.Middleware.Pipeline { + using Predicate = Func; + public static class OcelotPipelineBuilderExtensions { internal const string InvokeMethodName = "Invoke"; @@ -73,7 +76,28 @@ namespace Ocelot.Middleware.Pipeline var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs); if (parameters.Length == 1) { - return (OcelotRequestDelegate)methodinfo.CreateDelegate(typeof(OcelotRequestDelegate), instance); + var ocelotDelegate = (OcelotRequestDelegate)methodinfo.CreateDelegate(typeof(OcelotRequestDelegate), instance); + var diagnosticListener = (DiagnosticListener)app.ApplicationServices.GetService(typeof(DiagnosticListener)); + var middlewareName = ocelotDelegate.Target.GetType().Name; + + OcelotRequestDelegate wrapped = context => { + try + { + Write(diagnosticListener, "Ocelot.MiddlewareStarted", middlewareName, context); + return ocelotDelegate(context); + } + catch(Exception ex) + { + WriteException(diagnosticListener, ex, "Ocelot.MiddlewareException", middlewareName, context); + throw ex; + } + finally + { + Write(diagnosticListener, "Ocelot.MiddlewareFinished", middlewareName, context); + } + }; + + return wrapped; } var factory = Compile(methodinfo, parameters); @@ -91,10 +115,55 @@ namespace Ocelot.Middleware.Pipeline }); } + private static void Write(DiagnosticListener diagnosticListener, string message, string middlewareName, DownstreamContext context) + { + if(diagnosticListener != null) + { + diagnosticListener.Write(message, new { name = middlewareName, context = context }); + } + } + + private static void WriteException(DiagnosticListener diagnosticListener, Exception exception, string message, string middlewareName, DownstreamContext context) + { + if(diagnosticListener != null) + { + diagnosticListener.Write(message, new { name = middlewareName, context = context, exception = exception }); + } + } + + public static IOcelotPipelineBuilder MapWhen(this IOcelotPipelineBuilder app, Predicate predicate, Action configuration) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + var branchBuilder = app.New(); + configuration(branchBuilder); + var branch = branchBuilder.Build(); + + var options = new MapWhenOptions + { + Predicate = predicate, + Branch = branch, + }; + return app.Use(next => new MapWhenMiddleware(next, options).Invoke); + } + private static Func Compile(MethodInfo methodinfo, ParameterInfo[] parameters) { var middleware = typeof(T); - var httpContextArg = Expression.Parameter(typeof(HttpContext), "httpContext"); + var httpContextArg = Expression.Parameter(typeof(DownstreamContext), "downstreamContext"); var providerArg = Expression.Parameter(typeof(IServiceProvider), "serviceProvider"); var instanceArg = Expression.Parameter(middleware, "middleware"); @@ -111,8 +180,7 @@ namespace Ocelot.Middleware.Pipeline var parameterTypeExpression = new Expression[] { providerArg, - Expression.Constant(parameterType, typeof(Type)), - Expression.Constant(methodinfo.DeclaringType, typeof(Type)) + Expression.Constant(parameterType, typeof(Type)) }; var getServiceCall = Expression.Call(GetServiceInfo, parameterTypeExpression); diff --git a/src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs b/src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs index 374b867f..f9a74235 100644 --- a/src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs +++ b/src/Ocelot/Middleware/Pipeline/OcelotPipelineExtensions.cs @@ -15,18 +15,30 @@ using Ocelot.Request.Middleware; using Ocelot.Requester.Middleware; using Ocelot.RequestId.Middleware; using Ocelot.Responder.Middleware; +using Ocelot.WebSockets.Middleware; namespace Ocelot.Middleware.Pipeline { public static class OcelotPipelineExtensions { public static OcelotRequestDelegate BuildOcelotPipeline(this IOcelotPipelineBuilder builder, - OcelotPipelineConfiguration pipelineConfiguration = null) + OcelotPipelineConfiguration pipelineConfiguration) { // This is registered to catch any global exceptions that are not handled // It also sets the Request Id if anything is set globally builder.UseExceptionHandlerMiddleware(); + // If the request is for websockets upgrade we fork into a different pipeline + builder.MapWhen(context => context.HttpContext.WebSockets.IsWebSocketRequest, + app => + { + app.UseDownstreamRouteFinderMiddleware(); + app.UseDownstreamRequestInitialiser(); + app.UseLoadBalancingMiddleware(); + app.UseDownstreamUrlCreatorMiddleware(); + app.UseWebSocketsProxyMiddleware(); + }); + // Allow the user to respond with absolutely anything they want. builder.UseIfNotNull(pipelineConfiguration.PreErrorResponderMiddleware); diff --git a/src/Ocelot/Ocelot.csproj b/src/Ocelot/Ocelot.csproj index 89ada54d..833887a2 100644 --- a/src/Ocelot/Ocelot.csproj +++ b/src/Ocelot/Ocelot.csproj @@ -3,6 +3,7 @@ netcoreapp2.0 2.0.0 2.0.0 + true 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. Ocelot 0.0.0-dev @@ -25,27 +26,28 @@ - - - - - - - - - - - + + + + + + + + + + + all - - - - - - + + + + + + + - \ No newline at end of file + diff --git a/src/Ocelot/QueryStrings/AddQueriesToRequest.cs b/src/Ocelot/QueryStrings/AddQueriesToRequest.cs index 25e9772a..3cc2abdf 100644 --- a/src/Ocelot/QueryStrings/AddQueriesToRequest.cs +++ b/src/Ocelot/QueryStrings/AddQueriesToRequest.cs @@ -7,6 +7,9 @@ using Ocelot.Responses; using System.Security.Claims; using System.Net.Http; using System; +using Ocelot.Request.Middleware; +using Microsoft.Extensions.Primitives; +using System.Text; namespace Ocelot.QueryStrings { @@ -19,9 +22,9 @@ namespace Ocelot.QueryStrings _claimsParser = claimsParser; } - public Response SetQueriesOnDownstreamRequest(List claimsToThings, IEnumerable claims, HttpRequestMessage downstreamRequest) + public Response SetQueriesOnDownstreamRequest(List claimsToThings, IEnumerable claims, DownstreamRequest downstreamRequest) { - var queryDictionary = ConvertQueryStringToDictionary(downstreamRequest.RequestUri.Query); + var queryDictionary = ConvertQueryStringToDictionary(downstreamRequest.Query); foreach (var config in claimsToThings) { @@ -44,24 +47,48 @@ namespace Ocelot.QueryStrings } } - var uriBuilder = new UriBuilder(downstreamRequest.RequestUri); - uriBuilder.Query = ConvertDictionaryToQueryString(queryDictionary); - - downstreamRequest.RequestUri = uriBuilder.Uri; + downstreamRequest.Query = ConvertDictionaryToQueryString(queryDictionary); return new OkResponse(); } - private Dictionary ConvertQueryStringToDictionary(string queryString) + private Dictionary ConvertQueryStringToDictionary(string queryString) { - return Microsoft.AspNetCore.WebUtilities.QueryHelpers - .ParseQuery(queryString) - .ToDictionary(q => q.Key, q => q.Value.FirstOrDefault() ?? string.Empty); + var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers + .ParseQuery(queryString); + + return query; } - private string ConvertDictionaryToQueryString(Dictionary queryDictionary) + private string ConvertDictionaryToQueryString(Dictionary queryDictionary) { - return Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString("", queryDictionary); + var builder = new StringBuilder(); + + builder.Append("?"); + + int outerCount = 0; + + foreach (var query in queryDictionary) + { + for (int innerCount = 0; innerCount < query.Value.Count; innerCount++) + { + builder.Append($"{query.Key}={query.Value[innerCount]}"); + + if(innerCount < (query.Value.Count - 1)) + { + builder.Append("&"); + } + } + + if(outerCount < (queryDictionary.Count - 1)) + { + builder.Append("&"); + } + + outerCount++; + } + + return builder.ToString(); } } -} \ No newline at end of file +} diff --git a/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs b/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs index 34a6c2f5..bc017936 100644 --- a/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs +++ b/src/Ocelot/QueryStrings/IAddQueriesToRequest.cs @@ -4,11 +4,12 @@ using Ocelot.Configuration; using Ocelot.Responses; using System.Net.Http; using System.Security.Claims; +using Ocelot.Request.Middleware; namespace Ocelot.QueryStrings { public interface IAddQueriesToRequest { - Response SetQueriesOnDownstreamRequest(List claimsToThings, IEnumerable claims, HttpRequestMessage downstreamRequest); + Response SetQueriesOnDownstreamRequest(List claimsToThings, IEnumerable claims, DownstreamRequest downstreamRequest); } } diff --git a/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs b/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs index ae5dca85..bc886c5d 100644 --- a/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs +++ b/src/Ocelot/QueryStrings/Middleware/QueryStringBuilderMiddleware.cs @@ -12,28 +12,27 @@ namespace Ocelot.QueryStrings.Middleware { private readonly OcelotRequestDelegate _next; private readonly IAddQueriesToRequest _addQueriesToRequest; - private readonly IOcelotLogger _logger; public QueryStringBuilderMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IAddQueriesToRequest addQueriesToRequest) + : base(loggerFactory.CreateLogger()) { _next = next; _addQueriesToRequest = addQueriesToRequest; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) { if (context.DownstreamReRoute.ClaimsToQueries.Any()) { - _logger.LogDebug($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} has instructions to convert claims to queries"); + Logger.LogInformation($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} has instructions to convert claims to queries"); var response = _addQueriesToRequest.SetQueriesOnDownstreamRequest(context.DownstreamReRoute.ClaimsToQueries, context.HttpContext.User.Claims, context.DownstreamRequest); if (response.IsError) { - _logger.LogDebug("there was an error setting queries on context, setting pipeline error"); + Logger.LogWarning("there was an error setting queries on context, setting pipeline error"); SetPipelineError(context, response.Errors); return; diff --git a/src/Ocelot/Raft/FilePeersProvider.cs b/src/Ocelot/Raft/FilePeersProvider.cs index d31dc2ca..52b877df 100644 --- a/src/Ocelot/Raft/FilePeersProvider.cs +++ b/src/Ocelot/Raft/FilePeersProvider.cs @@ -1,10 +1,8 @@ -using System; using System.Collections.Generic; using System.Net.Http; -using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Options; using Ocelot.Configuration; -using Ocelot.Configuration.Provider; +using Ocelot.Configuration.Repository; using Ocelot.Middleware; using Rafty.Concensus; using Rafty.Infrastructure; @@ -15,21 +13,20 @@ namespace Ocelot.Raft public class FilePeersProvider : IPeersProvider { private readonly IOptions _options; - private List _peers; + private readonly List _peers; private IBaseUrlFinder _finder; - private IOcelotConfigurationProvider _provider; + private IInternalConfigurationRepository _repo; private IIdentityServerConfiguration _identityServerConfig; - public FilePeersProvider(IOptions options, IBaseUrlFinder finder, IOcelotConfigurationProvider provider, IIdentityServerConfiguration identityServerConfig) + public FilePeersProvider(IOptions options, IBaseUrlFinder finder, IInternalConfigurationRepository repo, IIdentityServerConfiguration identityServerConfig) { _identityServerConfig = identityServerConfig; - _provider = provider; + _repo = repo; _finder = finder; _options = options; _peers = new List(); - //todo - sort out async nonsense.. - var config = _provider.Get().GetAwaiter().GetResult(); + var config = _repo.Get(); foreach (var item in _options.Value.Peers) { var httpClient = new HttpClient(); diff --git a/src/Ocelot/Raft/HttpPeer.cs b/src/Ocelot/Raft/HttpPeer.cs index cd5ceef5..fcec77ea 100644 --- a/src/Ocelot/Raft/HttpPeer.cs +++ b/src/Ocelot/Raft/HttpPeer.cs @@ -1,12 +1,8 @@ using System; using System.Collections.Generic; using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Hosting; using Newtonsoft.Json; -using Ocelot.Authentication; using Ocelot.Configuration; -using Ocelot.Configuration.Provider; using Ocelot.Middleware; using Rafty.Concensus; using Rafty.FiniteStateMachine; @@ -16,15 +12,15 @@ namespace Ocelot.Raft [ExcludeFromCoverage] public class HttpPeer : IPeer { - private string _hostAndPort; - private HttpClient _httpClient; - private JsonSerializerSettings _jsonSerializerSettings; - private string _baseSchemeUrlAndPort; + private readonly string _hostAndPort; + private readonly HttpClient _httpClient; + private readonly JsonSerializerSettings _jsonSerializerSettings; + private readonly string _baseSchemeUrlAndPort; private BearerToken _token; - private IOcelotConfiguration _config; - private IIdentityServerConfiguration _identityServerConfiguration; + private readonly IInternalConfiguration _config; + private readonly IIdentityServerConfiguration _identityServerConfiguration; - public HttpPeer(string hostAndPort, HttpClient httpClient, IBaseUrlFinder finder, IOcelotConfiguration config, IIdentityServerConfiguration identityServerConfiguration) + public HttpPeer(string hostAndPort, HttpClient httpClient, IBaseUrlFinder finder, IInternalConfiguration config, IIdentityServerConfiguration identityServerConfiguration) { _identityServerConfiguration = identityServerConfiguration; _config = config; diff --git a/src/Ocelot/Raft/RaftController.cs b/src/Ocelot/Raft/RaftController.cs index eb91e86f..c1222e4d 100644 --- a/src/Ocelot/Raft/RaftController.cs +++ b/src/Ocelot/Raft/RaftController.cs @@ -40,9 +40,13 @@ namespace Ocelot.Raft using(var reader = new StreamReader(HttpContext.Request.Body)) { var json = await reader.ReadToEndAsync(); + var appendEntries = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); + _logger.LogDebug($"{_baseSchemeUrlAndPort}/appendentries called, my state is {_node.State.GetType().FullName}"); + var appendEntriesResponse = _node.Handle(appendEntries); + return new OkObjectResult(appendEntriesResponse); } } @@ -53,9 +57,13 @@ namespace Ocelot.Raft using(var reader = new StreamReader(HttpContext.Request.Body)) { var json = await reader.ReadToEndAsync(); + var requestVote = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); + _logger.LogDebug($"{_baseSchemeUrlAndPort}/requestvote called, my state is {_node.State.GetType().FullName}"); + var requestVoteResponse = _node.Handle(requestVote); + return new OkObjectResult(requestVoteResponse); } } @@ -68,10 +76,15 @@ namespace Ocelot.Raft using(var reader = new StreamReader(HttpContext.Request.Body)) { var json = await reader.ReadToEndAsync(); + var command = JsonConvert.DeserializeObject(json, _jsonSerialiserSettings); + _logger.LogDebug($"{_baseSchemeUrlAndPort}/command called, my state is {_node.State.GetType().FullName}"); + var commandResponse = _node.Accept(command); + json = JsonConvert.SerializeObject(commandResponse, _jsonSerialiserSettings); + return StatusCode(200, json); } } diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs index 4ceb91f8..50eb7776 100644 --- a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs @@ -14,16 +14,15 @@ namespace Ocelot.RateLimit.Middleware public class ClientRateLimitMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; - private readonly IOcelotLogger _logger; private readonly IRateLimitCounterHandler _counterHandler; private readonly ClientRateLimitProcessor _processor; public ClientRateLimitMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IRateLimitCounterHandler counterHandler) + :base(loggerFactory.CreateLogger()) { _next = next; - _logger = loggerFactory.CreateLogger(); _counterHandler = counterHandler; _processor = new ClientRateLimitProcessor(counterHandler); } @@ -35,7 +34,7 @@ namespace Ocelot.RateLimit.Middleware // check if rate limiting is enabled if (!context.DownstreamReRoute.EnableEndpointEndpointRateLimiting) { - _logger.LogDebug($"EndpointRateLimiting is not enabled for {context.DownstreamReRoute.DownstreamPathTemplate}"); + Logger.LogInformation($"EndpointRateLimiting is not enabled for {context.DownstreamReRoute.DownstreamPathTemplate.Value}"); await _next.Invoke(context); return; } @@ -46,7 +45,7 @@ namespace Ocelot.RateLimit.Middleware // check white list if (IsWhitelisted(identity, options)) { - _logger.LogDebug($"{context.DownstreamReRoute.DownstreamPathTemplate} is white listed from rate limiting"); + Logger.LogInformation($"{context.DownstreamReRoute.DownstreamPathTemplate.Value} is white listed from rate limiting"); await _next.Invoke(context); return; } @@ -112,9 +111,10 @@ namespace Ocelot.RateLimit.Middleware public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule, DownstreamReRoute downstreamReRoute) { - _logger.LogDebug($"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule { downstreamReRoute.UpstreamPathTemplate }, TraceIdentifier {httpContext.TraceIdentifier}."); + Logger.LogInformation( + $"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule { downstreamReRoute.UpstreamPathTemplate.Value }, TraceIdentifier {httpContext.TraceIdentifier}."); } - + public virtual Task ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter) { var message = string.IsNullOrEmpty(option.QuotaExceededMessage) ? $"API calls quota exceeded! maximum admitted {option.RateLimitRule.Limit} per {option.RateLimitRule.Period}." : option.QuotaExceededMessage; diff --git a/src/Ocelot/Request/Mapper/RequestMapper.cs b/src/Ocelot/Request/Mapper/RequestMapper.cs index ee04a551..67afc53d 100644 --- a/src/Ocelot/Request/Mapper/RequestMapper.cs +++ b/src/Ocelot/Request/Mapper/RequestMapper.cs @@ -5,9 +5,7 @@ using System.IO; using System.Linq; using System.Net.Http; - using System.Net.Http.Headers; using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Primitives; @@ -83,7 +81,6 @@ { foreach (var header in request.Headers) { - //todo get rid of if.. if (IsSupportedHeader(header)) { requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); diff --git a/src/Ocelot/Request/Mapper/UnmappableRequestError.cs b/src/Ocelot/Request/Mapper/UnmappableRequestError.cs index 79379b6b..81668eef 100644 --- a/src/Ocelot/Request/Mapper/UnmappableRequestError.cs +++ b/src/Ocelot/Request/Mapper/UnmappableRequestError.cs @@ -5,7 +5,7 @@ public class UnmappableRequestError : Error { - public UnmappableRequestError(Exception ex) : base($"Error when parsing incoming request, exception: {ex.Message}", OcelotErrorCode.UnmappableRequestError) + public UnmappableRequestError(Exception exception) : base($"Error when parsing incoming request, exception: {exception}", OcelotErrorCode.UnmappableRequestError) { } } diff --git a/src/Ocelot/Request/Middleware/DownstreamRequest.cs b/src/Ocelot/Request/Middleware/DownstreamRequest.cs new file mode 100644 index 00000000..75070bfd --- /dev/null +++ b/src/Ocelot/Request/Middleware/DownstreamRequest.cs @@ -0,0 +1,74 @@ +namespace Ocelot.Request.Middleware +{ + using System; + using System.Net.Http; + using System.Net.Http.Headers; + + public class DownstreamRequest + { + private readonly HttpRequestMessage _request; + + public DownstreamRequest(HttpRequestMessage request) + { + _request = request; + Method = _request.Method.Method; + OriginalString = _request.RequestUri.OriginalString; + Scheme = _request.RequestUri.Scheme; + Host = _request.RequestUri.Host; + Port = _request.RequestUri.Port; + Headers = _request.Headers; + AbsolutePath = _request.RequestUri.AbsolutePath; + Query = _request.RequestUri.Query; + } + + public HttpRequestHeaders Headers { get; } + + public string Method { get; } + + public string OriginalString { get; } + + public string Scheme { get; set; } + + public string Host { get; set; } + + public int Port { get; set; } + + public string AbsolutePath { get; set; } + + public string Query { get; set; } + + public HttpRequestMessage ToHttpRequestMessage() + { + var uriBuilder = new UriBuilder + { + Port = Port, + Host = Host, + Path = AbsolutePath, + Query = Query, + Scheme = Scheme + }; + + _request.RequestUri = uriBuilder.Uri; + return _request; + } + + public string ToUri() + { + var uriBuilder = new UriBuilder + { + Port = Port, + Host = Host, + Path = AbsolutePath, + Query = Query, + Scheme = Scheme + }; + + return uriBuilder.Uri.AbsoluteUri; + } + + public override string ToString() + { + return ToUri(); + } + } +} diff --git a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs index f14c1394..0cb7c7dd 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs @@ -1,5 +1,6 @@ namespace Ocelot.Request.Middleware { + using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Ocelot.DownstreamRouteFinder.Middleware; @@ -10,15 +11,14 @@ namespace Ocelot.Request.Middleware public class DownstreamRequestInitialiserMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; - private readonly IOcelotLogger _logger; private readonly Mapper.IRequestMapper _requestMapper; public DownstreamRequestInitialiserMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, Mapper.IRequestMapper requestMapper) + :base(loggerFactory.CreateLogger()) { _next = next; - _logger = loggerFactory.CreateLogger(); _requestMapper = requestMapper; } @@ -31,9 +31,9 @@ namespace Ocelot.Request.Middleware return; } - context.DownstreamRequest = downstreamRequest.Data; + context.DownstreamRequest = new DownstreamRequest(downstreamRequest.Data); await _next.Invoke(context); } } -} \ No newline at end of file +} diff --git a/src/Ocelot/RequestId/Middleware/ReRouteRequestIdMiddleware.cs b/src/Ocelot/RequestId/Middleware/ReRouteRequestIdMiddleware.cs index 32ee6c68..b9ddf824 100644 --- a/src/Ocelot/RequestId/Middleware/ReRouteRequestIdMiddleware.cs +++ b/src/Ocelot/RequestId/Middleware/ReRouteRequestIdMiddleware.cs @@ -1,30 +1,27 @@ +using System; 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; -using System.Net.Http; using System.Net.Http.Headers; using System.Collections.Generic; -using Ocelot.DownstreamRouteFinder.Middleware; +using Ocelot.Request.Middleware; namespace Ocelot.RequestId.Middleware { public class ReRouteRequestIdMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; - private readonly IOcelotLogger _logger; private readonly IRequestScopedDataRepository _requestScopedDataRepository; public ReRouteRequestIdMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IRequestScopedDataRepository requestScopedDataRepository) + : base(loggerFactory.CreateLogger()) { _next = next; _requestScopedDataRepository = requestScopedDataRepository; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) @@ -35,32 +32,24 @@ namespace Ocelot.RequestId.Middleware private void SetOcelotRequestId(DownstreamContext context) { - // if get request ID is set on upstream request then retrieve it var key = context.DownstreamReRoute.RequestIdKey ?? DefaultRequestIdKey.Value; - - StringValues upstreamRequestIds; - if (context.HttpContext.Request.Headers.TryGetValue(key, out upstreamRequestIds)) + + if (context.HttpContext.Request.Headers.TryGetValue(key, out var upstreamRequestIds)) { - //set the traceidentifier context.HttpContext.TraceIdentifier = upstreamRequestIds.First(); - //todo fix looking in both places - //check if we have previous id in scoped repo var previousRequestId = _requestScopedDataRepository.Get("RequestId"); - if (!previousRequestId.IsError && !string.IsNullOrEmpty(previousRequestId.Data)) + if (!previousRequestId.IsError && !string.IsNullOrEmpty(previousRequestId.Data) && previousRequestId.Data != context.HttpContext.TraceIdentifier) { - //we have a previous request id lets store it and update request id - _requestScopedDataRepository.Add("PreviousRequestId", previousRequestId.Data); - _requestScopedDataRepository.Update("RequestId", context.HttpContext.TraceIdentifier); + _requestScopedDataRepository.Add("PreviousRequestId", previousRequestId.Data); + _requestScopedDataRepository.Update("RequestId", context.HttpContext.TraceIdentifier); } else { - //else just add request id - _requestScopedDataRepository.Add("RequestId", context.HttpContext.TraceIdentifier); + _requestScopedDataRepository.Add("RequestId", context.HttpContext.TraceIdentifier); } } - // set request ID on downstream request, if required var requestId = new RequestId(context.DownstreamReRoute.RequestIdKey, context.HttpContext.TraceIdentifier); if (ShouldAddRequestId(requestId, context.DownstreamRequest.Headers)) @@ -82,7 +71,7 @@ namespace Ocelot.RequestId.Middleware return headers.TryGetValues(requestId.RequestIdKey, out value); } - private void AddRequestIdHeader(RequestId requestId, HttpRequestMessage httpRequestMessage) + private void AddRequestIdHeader(RequestId requestId, DownstreamRequest httpRequestMessage) { httpRequestMessage.Headers.Add(requestId.RequestIdKey, requestId.RequestIdValue); } diff --git a/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs b/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs index e0d9da82..02969855 100644 --- a/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs +++ b/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using Butterfly.Client.Tracing; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; diff --git a/src/Ocelot/Requester/FakeServiceTracer.cs b/src/Ocelot/Requester/FakeServiceTracer.cs new file mode 100644 index 00000000..95347ef6 --- /dev/null +++ b/src/Ocelot/Requester/FakeServiceTracer.cs @@ -0,0 +1,17 @@ +using Butterfly.Client.Tracing; +using Butterfly.OpenTracing; + +namespace Ocelot.Requester +{ + public class FakeServiceTracer : IServiceTracer + { + public ITracer Tracer { get; } + public string ServiceName { get; } + public string Environment { get; } + public string Identity { get; } + public ISpan Start(ISpanBuilder spanBuilder) + { + return null; + } + } +} diff --git a/src/Ocelot/Requester/GlobalDelegatingHandler.cs b/src/Ocelot/Requester/GlobalDelegatingHandler.cs new file mode 100644 index 00000000..ba5e1c5f --- /dev/null +++ b/src/Ocelot/Requester/GlobalDelegatingHandler.cs @@ -0,0 +1,14 @@ +using System.Net.Http; + +namespace Ocelot.Requester +{ + public class GlobalDelegatingHandler + { + public GlobalDelegatingHandler(DelegatingHandler delegatingHandler) + { + DelegatingHandler = delegatingHandler; + } + + public DelegatingHandler DelegatingHandler { get; private set; } + } +} diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index b5604081..fdc90ebf 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -1,25 +1,80 @@ -using System.Linq; +using System; +using System.Linq; +using System.Net; using System.Net.Http; using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Middleware; namespace Ocelot.Requester { public class HttpClientBuilder : IHttpClientBuilder { private readonly IDelegatingHandlerHandlerFactory _factory; + private readonly IHttpClientCache _cacheHandlers; + private readonly IOcelotLogger _logger; + private string _cacheKey; + private HttpClient _httpClient; + private IHttpClient _client; + private readonly TimeSpan _defaultTimeout; - public HttpClientBuilder(IDelegatingHandlerHandlerFactory house) + public HttpClientBuilder( + IDelegatingHandlerHandlerFactory factory, + IHttpClientCache cacheHandlers, + IOcelotLogger logger) { - _factory = house; + _factory = factory; + _cacheHandlers = cacheHandlers; + _logger = logger; + + // This is hardcoded at the moment but can easily be added to configuration + // if required by a user request. + _defaultTimeout = TimeSpan.FromSeconds(90); } - public IHttpClient Create(DownstreamReRoute request) + public IHttpClient Create(DownstreamContext context) { - var httpclientHandler = new HttpClientHandler { AllowAutoRedirect = request.HttpHandlerOptions.AllowAutoRedirect, UseCookies = request.HttpHandlerOptions.UseCookieContainer}; - - var client = new HttpClient(CreateHttpMessageHandler(httpclientHandler, request)); - - return new HttpClientWrapper(client); + _cacheKey = GetCacheKey(context); + + var httpClient = _cacheHandlers.Get(_cacheKey); + + if (httpClient != null) + { + return httpClient; + } + + var httpclientHandler = new HttpClientHandler + { + AllowAutoRedirect = context.DownstreamReRoute.HttpHandlerOptions.AllowAutoRedirect, + UseCookies = context.DownstreamReRoute.HttpHandlerOptions.UseCookieContainer, + CookieContainer = new CookieContainer() + }; + + if(context.DownstreamReRoute.DangerousAcceptAnyServerCertificateValidator) + { + httpclientHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + + _logger + .LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamReRoute, UpstreamPathTemplate: {context.DownstreamReRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {context.DownstreamReRoute.DownstreamPathTemplate}"); + } + + var timeout = context.DownstreamReRoute.QosOptionsOptions.TimeoutValue == 0 + ? _defaultTimeout + : TimeSpan.FromMilliseconds(context.DownstreamReRoute.QosOptionsOptions.TimeoutValue); + + _httpClient = new HttpClient(CreateHttpMessageHandler(httpclientHandler, context.DownstreamReRoute)) + { + Timeout = timeout + }; + + _client = new HttpClientWrapper(_httpClient); + + return _client; + } + + public void Save() + { + _cacheHandlers.Set(_cacheKey, _client, TimeSpan.FromHours(24)); } private HttpMessageHandler CreateHttpMessageHandler(HttpMessageHandler httpMessageHandler, DownstreamReRoute request) @@ -39,5 +94,10 @@ namespace Ocelot.Requester }); return httpMessageHandler; } + + private string GetCacheKey(DownstreamContext request) + { + return request.DownstreamRequest.OriginalString; + } } } diff --git a/src/Ocelot/Requester/HttpClientHttpRequester.cs b/src/Ocelot/Requester/HttpClientHttpRequester.cs index c60e3b78..c7914c94 100644 --- a/src/Ocelot/Requester/HttpClientHttpRequester.cs +++ b/src/Ocelot/Requester/HttpClientHttpRequester.cs @@ -24,28 +24,28 @@ namespace Ocelot.Requester _factory = house; } - public async Task> GetResponse(DownstreamContext request) + public async Task> GetResponse(DownstreamContext context) { - var builder = new HttpClientBuilder(_factory); + var builder = new HttpClientBuilder(_factory, _cacheHandlers, _logger); - var cacheKey = GetCacheKey(request); - - var httpClient = GetHttpClient(cacheKey, builder, request); + var httpClient = builder.Create(context); try { - var response = await httpClient.SendAsync(request.DownstreamRequest); + var response = await httpClient.SendAsync(context.DownstreamRequest.ToHttpRequestMessage()); return new OkResponse(response); } catch (TimeoutRejectedException exception) { - return - new ErrorResponse(new RequestTimedOutError(exception)); + return new ErrorResponse(new RequestTimedOutError(exception)); + } + catch (TaskCanceledException exception) + { + return new ErrorResponse(new RequestTimedOutError(exception)); } catch (BrokenCircuitException exception) { - return - new ErrorResponse(new RequestTimedOutError(exception)); + return new ErrorResponse(new RequestTimedOutError(exception)); } catch (Exception exception) { @@ -53,42 +53,8 @@ namespace Ocelot.Requester } finally { - _cacheHandlers.Set(cacheKey, httpClient, TimeSpan.FromHours(24)); + builder.Save(); } } - - private IHttpClient GetHttpClient(string cacheKey, IHttpClientBuilder builder, DownstreamContext request) - { - var httpClient = _cacheHandlers.Get(cacheKey); - - if (httpClient == null) - { - httpClient = builder.Create(request.DownstreamReRoute); - } - - return httpClient; - } - - private string GetCacheKey(DownstreamContext request) - { - var baseUrl = $"{request.DownstreamRequest.RequestUri.Scheme}://{request.DownstreamRequest.RequestUri.Authority}"; - - return baseUrl; - } - } - - public class ReRouteDelegatingHandler where T : DelegatingHandler - { - public T DelegatingHandler { get; private set; } - } - - public class GlobalDelegatingHandler - { - public GlobalDelegatingHandler(DelegatingHandler delegatingHandler) - { - DelegatingHandler = delegatingHandler; - } - - public DelegatingHandler DelegatingHandler { get; private set; } } } diff --git a/src/Ocelot/Requester/HttpClientWrapper.cs b/src/Ocelot/Requester/HttpClientWrapper.cs index 21e74e48..b1f0345a 100644 --- a/src/Ocelot/Requester/HttpClientWrapper.cs +++ b/src/Ocelot/Requester/HttpClientWrapper.cs @@ -6,7 +6,7 @@ namespace Ocelot.Requester /// /// This class was made to make unit testing easier when HttpClient is used. /// - internal class HttpClientWrapper : IHttpClient + public class HttpClientWrapper : IHttpClient { public HttpClient Client { get; } diff --git a/src/Ocelot/Requester/IDelegatingHandlerHandlerProviderFactory.cs b/src/Ocelot/Requester/IDelegatingHandlerHandlerFactory.cs similarity index 100% rename from src/Ocelot/Requester/IDelegatingHandlerHandlerProviderFactory.cs rename to src/Ocelot/Requester/IDelegatingHandlerHandlerFactory.cs diff --git a/src/Ocelot/Requester/IHttpClientBuilder.cs b/src/Ocelot/Requester/IHttpClientBuilder.cs index f10e55f5..cc8c6160 100644 --- a/src/Ocelot/Requester/IHttpClientBuilder.cs +++ b/src/Ocelot/Requester/IHttpClientBuilder.cs @@ -1,14 +1,10 @@ -using System.Net.Http; -using Ocelot.Configuration; +using Ocelot.Middleware; namespace Ocelot.Requester { public interface IHttpClientBuilder { - /// - /// Creates the - /// - /// - IHttpClient Create(DownstreamReRoute request); + IHttpClient Create(DownstreamContext request); + void Save(); } } diff --git a/src/Ocelot/Requester/IHttpRequester.cs b/src/Ocelot/Requester/IHttpRequester.cs index 5d9aa5dc..86f6209a 100644 --- a/src/Ocelot/Requester/IHttpRequester.cs +++ b/src/Ocelot/Requester/IHttpRequester.cs @@ -7,6 +7,6 @@ namespace Ocelot.Requester { public interface IHttpRequester { - Task> GetResponse(DownstreamContext request); + Task> GetResponse(DownstreamContext context); } } diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs index 68397b9f..4204374b 100644 --- a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs @@ -1,10 +1,6 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using System.Threading.Tasks; -using Ocelot.DownstreamRouteFinder.Middleware; -using Ocelot.Requester.QoS; namespace Ocelot.Requester.Middleware { @@ -12,15 +8,14 @@ namespace Ocelot.Requester.Middleware { private readonly OcelotRequestDelegate _next; private readonly IHttpRequester _requester; - private readonly IOcelotLogger _logger; public HttpRequesterMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpRequester requester) + : base(loggerFactory.CreateLogger()) { _next = next; _requester = requester; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) @@ -29,15 +24,15 @@ namespace Ocelot.Requester.Middleware if (response.IsError) { - _logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); + Logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); SetPipelineError(context, response.Errors); return; } - _logger.LogDebug("setting http response message"); + Logger.LogDebug("setting http response message"); - context.DownstreamResponse = response.Data; + context.DownstreamResponse = new DownstreamResponse(response.Data); } } } diff --git a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs index 33bd4caf..fd6dce2b 100644 --- a/src/Ocelot/Requester/OcelotHttpTracingHandler.cs +++ b/src/Ocelot/Requester/OcelotHttpTracingHandler.cs @@ -1,38 +1,49 @@ using System; -using System.Collections.Generic; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Butterfly.Client.Tracing; using Butterfly.OpenTracing; +using Ocelot.Infrastructure.RequestData; namespace Ocelot.Requester { public class OcelotHttpTracingHandler : DelegatingHandler, ITracingHandler { private readonly IServiceTracer _tracer; - private const string prefix_spanId = "ot-spanId"; + private readonly IRequestScopedDataRepository _repo; + private const string PrefixSpanId = "ot-spanId"; - public OcelotHttpTracingHandler(IServiceTracer tracer, HttpMessageHandler httpMessageHandler = null) + public OcelotHttpTracingHandler( + IServiceTracer tracer, + IRequestScopedDataRepository repo, + HttpMessageHandler httpMessageHandler = null) { + _repo = repo; _tracer = tracer ?? throw new ArgumentNullException(nameof(tracer)); InnerHandler = httpMessageHandler ?? new HttpClientHandler(); } - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) { return _tracer.ChildTraceAsync($"httpclient {request.Method}", DateTimeOffset.UtcNow, span => TracingSendAsync(span, request, cancellationToken)); } - protected virtual async Task TracingSendAsync(ISpan span, HttpRequestMessage request, CancellationToken cancellationToken) + protected virtual async Task TracingSendAsync( + ISpan span, + HttpRequestMessage request, + CancellationToken cancellationToken) { - IEnumerable traceIdVals = null; - if (request.Headers.TryGetValues(prefix_spanId, out traceIdVals)) + if (request.Headers.Contains(PrefixSpanId)) { - request.Headers.Remove(prefix_spanId); - request.Headers.TryAddWithoutValidation(prefix_spanId, span.SpanContext.SpanId); + request.Headers.Remove(PrefixSpanId); + request.Headers.TryAddWithoutValidation(PrefixSpanId, span.SpanContext.SpanId); } + _repo.Add("TraceId", span.SpanContext.TraceId); + span.Tags.Client().Component("HttpClient") .HttpMethod(request.Method.Method) .HttpUrl(request.RequestUri.OriginalString) diff --git a/src/Ocelot/Requester/ReRouteDelegatingHandler.cs b/src/Ocelot/Requester/ReRouteDelegatingHandler.cs new file mode 100644 index 00000000..0a5c5472 --- /dev/null +++ b/src/Ocelot/Requester/ReRouteDelegatingHandler.cs @@ -0,0 +1,10 @@ +using System.Net.Http; + +namespace Ocelot.Requester +{ + public class ReRouteDelegatingHandler + where T : DelegatingHandler + { + public T DelegatingHandler { get; private set; } + } +} diff --git a/src/Ocelot/Requester/RequestTimedOutError.cs b/src/Ocelot/Requester/RequestTimedOutError.cs index 86eab0c6..f99308b3 100644 --- a/src/Ocelot/Requester/RequestTimedOutError.cs +++ b/src/Ocelot/Requester/RequestTimedOutError.cs @@ -6,7 +6,7 @@ namespace Ocelot.Requester public class RequestTimedOutError : Error { public RequestTimedOutError(Exception exception) - : base($"Timeout making http request, exception: {exception.Message}", OcelotErrorCode.RequestTimedOutError) + : base($"Timeout making http request, exception: {exception}", OcelotErrorCode.RequestTimedOutError) { } } diff --git a/src/Ocelot/Requester/TracingHandlerFactory.cs b/src/Ocelot/Requester/TracingHandlerFactory.cs index 5cb72a79..f0eb97b1 100644 --- a/src/Ocelot/Requester/TracingHandlerFactory.cs +++ b/src/Ocelot/Requester/TracingHandlerFactory.cs @@ -1,32 +1,24 @@ using Butterfly.Client.Tracing; -using Butterfly.OpenTracing; +using Ocelot.Infrastructure.RequestData; namespace Ocelot.Requester { public class TracingHandlerFactory : ITracingHandlerFactory { private readonly IServiceTracer _tracer; + private readonly IRequestScopedDataRepository _repo; - public TracingHandlerFactory(IServiceTracer tracer) + public TracingHandlerFactory( + IServiceTracer tracer, + IRequestScopedDataRepository repo) { + _repo = repo; _tracer = tracer; } public ITracingHandler Get() { - return new OcelotHttpTracingHandler(_tracer); - } - } - - public class FakeServiceTracer : IServiceTracer - { - public ITracer Tracer { get; } - public string ServiceName { get; } - public string Environment { get; } - public string Identity { get; } - public ISpan Start(ISpanBuilder spanBuilder) - { - throw new System.NotImplementedException(); + return new OcelotHttpTracingHandler(_tracer, _repo); } } } diff --git a/src/Ocelot/Requester/UnableToCompleteRequestError.cs b/src/Ocelot/Requester/UnableToCompleteRequestError.cs index 033ca53f..c5eb7b31 100644 --- a/src/Ocelot/Requester/UnableToCompleteRequestError.cs +++ b/src/Ocelot/Requester/UnableToCompleteRequestError.cs @@ -6,7 +6,7 @@ namespace Ocelot.Requester public class UnableToCompleteRequestError : Error { public UnableToCompleteRequestError(Exception exception) - : base($"Error making http request, exception: {exception.Message}", OcelotErrorCode.UnableToCompleteRequestError) + : base($"Error making http request, exception: {exception}", OcelotErrorCode.UnableToCompleteRequestError) { } } diff --git a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs index be7c59b9..61d27de0 100644 --- a/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs +++ b/src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs @@ -1,38 +1,38 @@ -using System.Collections.Generic; -using System.Linq; -using Ocelot.Errors; - -namespace Ocelot.Responder -{ - public class ErrorsToHttpStatusCodeMapper : IErrorsToHttpStatusCodeMapper - { - public int Map(List errors) - { - if (errors.Any(e => e.Code == OcelotErrorCode.UnauthenticatedError)) - { - return 401; - } - - if (errors.Any(e => e.Code == OcelotErrorCode.UnauthorizedError - || e.Code == OcelotErrorCode.ClaimValueNotAuthorisedError - || e.Code == OcelotErrorCode.ScopeNotAuthorisedError - || e.Code == OcelotErrorCode.UserDoesNotHaveClaimError - || e.Code == OcelotErrorCode.CannotFindClaimError)) - { - return 403; - } - - if (errors.Any(e => e.Code == OcelotErrorCode.RequestTimedOutError)) - { - return 503; +using System.Collections.Generic; +using System.Linq; +using Ocelot.Errors; + +namespace Ocelot.Responder +{ + public class ErrorsToHttpStatusCodeMapper : IErrorsToHttpStatusCodeMapper + { + public int Map(List errors) + { + if (errors.Any(e => e.Code == OcelotErrorCode.UnauthenticatedError)) + { + return 401; + } + + if (errors.Any(e => e.Code == OcelotErrorCode.UnauthorizedError + || e.Code == OcelotErrorCode.ClaimValueNotAuthorisedError + || e.Code == OcelotErrorCode.ScopeNotAuthorisedError + || e.Code == OcelotErrorCode.UserDoesNotHaveClaimError + || e.Code == OcelotErrorCode.CannotFindClaimError)) + { + return 403; + } + + if (errors.Any(e => e.Code == OcelotErrorCode.RequestTimedOutError)) + { + return 503; + } + + if (errors.Any(e => e.Code == OcelotErrorCode.UnableToFindDownstreamRouteError)) + { + return 404; } - if (errors.Any(e => e.Code == OcelotErrorCode.UnableToFindDownstreamRouteError)) - { - return 404; - } - return 404; - } - } -} \ No newline at end of file + } + } +} diff --git a/src/Ocelot/Responder/HttpContextResponder.cs b/src/Ocelot/Responder/HttpContextResponder.cs index 7bc55d94..bd265a61 100644 --- a/src/Ocelot/Responder/HttpContextResponder.cs +++ b/src/Ocelot/Responder/HttpContextResponder.cs @@ -1,17 +1,14 @@ using System.IO; using System.Linq; using System.Net; -using System.Net.Http; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Ocelot.Headers; -using Ocelot.Responses; +using Ocelot.Middleware; 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 @@ -25,7 +22,7 @@ namespace Ocelot.Responder _removeOutputHeaders = removeOutputHeaders; } - public async Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response) + public async Task SetResponseOnHttpContext(HttpContext context, DownstreamResponse response) { _removeOutputHeaders.Remove(response.Headers); @@ -36,12 +33,12 @@ namespace Ocelot.Responder foreach (var httpResponseHeader in response.Content.Headers) { - AddHeaderIfDoesntExist(context, httpResponseHeader); + AddHeaderIfDoesntExist(context, new Header(httpResponseHeader.Key, httpResponseHeader.Value)); } var content = await response.Content.ReadAsByteArrayAsync(); - AddHeaderIfDoesntExist(context, new KeyValuePair>("Content-Length", new []{ content.Length.ToString() }) ); + AddHeaderIfDoesntExist(context, new Header("Content-Length", new []{ content.Length.ToString() }) ); context.Response.OnStarting(state => { @@ -70,11 +67,11 @@ namespace Ocelot.Responder }, context); } - private static void AddHeaderIfDoesntExist(HttpContext context, KeyValuePair> httpResponseHeader) + private static void AddHeaderIfDoesntExist(HttpContext context, Header httpResponseHeader) { if (!context.Response.Headers.ContainsKey(httpResponseHeader.Key)) { - context.Response.Headers.Add(httpResponseHeader.Key, new StringValues(httpResponseHeader.Value.ToArray())); + context.Response.Headers.Add(httpResponseHeader.Key, new StringValues(httpResponseHeader.Values.ToArray())); } } } diff --git a/src/Ocelot/Responder/IHttpResponder.cs b/src/Ocelot/Responder/IHttpResponder.cs index c5e5a0ca..73722f53 100644 --- a/src/Ocelot/Responder/IHttpResponder.cs +++ b/src/Ocelot/Responder/IHttpResponder.cs @@ -1,12 +1,13 @@ -using System.Net.Http; -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; namespace Ocelot.Responder { public interface IHttpResponder { - Task SetResponseOnHttpContext(HttpContext context, HttpResponseMessage response); + Task SetResponseOnHttpContext(HttpContext context, DownstreamResponse response); void SetErrorResponseOnContext(HttpContext context, int statusCode); } } diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs index 4bfbad7a..0487d811 100644 --- a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs +++ b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs @@ -1,11 +1,10 @@ using Microsoft.AspNetCore.Http; using Ocelot.Errors; -using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using System.Collections.Generic; using System.Threading.Tasks; -using Ocelot.DownstreamRouteFinder.Middleware; +using Ocelot.Infrastructure.Extensions; namespace Ocelot.Responder.Middleware { @@ -17,18 +16,17 @@ namespace Ocelot.Responder.Middleware private readonly OcelotRequestDelegate _next; private readonly IHttpResponder _responder; private readonly IErrorsToHttpStatusCodeMapper _codeMapper; - private readonly IOcelotLogger _logger; public ResponderMiddleware(OcelotRequestDelegate next, IHttpResponder responder, IOcelotLoggerFactory loggerFactory, IErrorsToHttpStatusCodeMapper codeMapper ) + :base(loggerFactory.CreateLogger()) { _next = next; _responder = responder; _codeMapper = codeMapper; - _logger = loggerFactory.CreateLogger(); } public async Task Invoke(DownstreamContext context) @@ -37,13 +35,13 @@ namespace Ocelot.Responder.Middleware if (context.IsError) { - var errors = context.Errors; - _logger.LogError($"{errors.Count} pipeline errors found in {MiddlewareName}. Setting error response status code"); - SetErrorResponse(context.HttpContext, errors); + Logger.LogWarning($"{context.Errors.ToErrorString()} errors found in {MiddlewareName}. Setting error response for request path:{context.HttpContext.Request.Path}, request method: {context.HttpContext.Request.Method}"); + + SetErrorResponse(context.HttpContext, context.Errors); } else { - _logger.LogDebug("no pipeline errors, setting and returning completed response"); + Logger.LogDebug("no pipeline errors, setting and returning completed response"); await _responder.SetResponseOnHttpContext(context.HttpContext, context.DownstreamResponse); } } diff --git a/src/Ocelot/Responses/ErrorResponse.cs b/src/Ocelot/Responses/ErrorResponse.cs index 7a6688e7..156243ab 100644 --- a/src/Ocelot/Responses/ErrorResponse.cs +++ b/src/Ocelot/Responses/ErrorResponse.cs @@ -5,11 +5,13 @@ namespace Ocelot.Responses { public class ErrorResponse : Response { - public ErrorResponse(Error error) : base(new List{error}) + public ErrorResponse(Error error) + : base(new List{error}) { } - public ErrorResponse(List errors) : base(errors) + public ErrorResponse(List errors) + : base(errors) { } } diff --git a/src/Ocelot/Responses/Response.cs b/src/Ocelot/Responses/Response.cs index 21ec91b5..3a387a2b 100644 --- a/src/Ocelot/Responses/Response.cs +++ b/src/Ocelot/Responses/Response.cs @@ -15,14 +15,8 @@ namespace Ocelot.Responses Errors = errors ?? new List(); } - public List Errors { get; private set; } + public List Errors { get; } - public bool IsError - { - get - { - return Errors.Count > 0; - } - } + public bool IsError => Errors.Count > 0; } -} \ No newline at end of file +} diff --git a/src/Ocelot/ServiceDiscovery/Configuration/ConsulRegistryConfiguration.cs b/src/Ocelot/ServiceDiscovery/Configuration/ConsulRegistryConfiguration.cs new file mode 100644 index 00000000..13ae68d2 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/Configuration/ConsulRegistryConfiguration.cs @@ -0,0 +1,18 @@ +namespace Ocelot.ServiceDiscovery.Configuration +{ + public class ConsulRegistryConfiguration + { + public ConsulRegistryConfiguration(string host, int port, string keyOfServiceInConsul, string token) + { + Host = host; + Port = port; + KeyOfServiceInConsul = keyOfServiceInConsul; + Token = token; + } + + public string KeyOfServiceInConsul { get; } + public string Host { get; } + public int Port { get; } + public string Token { get; } + } +} diff --git a/src/Ocelot/ServiceDiscovery/ServiceFabricConfiguration.cs b/src/Ocelot/ServiceDiscovery/Configuration/ServiceFabricConfiguration.cs similarity index 58% rename from src/Ocelot/ServiceDiscovery/ServiceFabricConfiguration.cs rename to src/Ocelot/ServiceDiscovery/Configuration/ServiceFabricConfiguration.cs index 7522a1e0..73211a5b 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceFabricConfiguration.cs +++ b/src/Ocelot/ServiceDiscovery/Configuration/ServiceFabricConfiguration.cs @@ -1,4 +1,4 @@ -namespace Ocelot.ServiceDiscovery +namespace Ocelot.ServiceDiscovery.Configuration { public class ServiceFabricConfiguration { @@ -9,8 +9,10 @@ ServiceName = serviceName; } - public string ServiceName { get; private set; } - public string HostName { get; private set; } - public int Port { get; private set; } + public string ServiceName { get; } + + public string HostName { get; } + + public int Port { get; } } } diff --git a/src/Ocelot/ServiceDiscovery/ConsulRegistryConfiguration.cs b/src/Ocelot/ServiceDiscovery/ConsulRegistryConfiguration.cs deleted file mode 100644 index ba389c05..00000000 --- a/src/Ocelot/ServiceDiscovery/ConsulRegistryConfiguration.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Ocelot.ServiceDiscovery -{ - public class ConsulRegistryConfiguration - { - public ConsulRegistryConfiguration(string hostName, int port, string keyOfServiceInConsul) - { - HostName = hostName; - Port = port; - KeyOfServiceInConsul = keyOfServiceInConsul; - } - - public string KeyOfServiceInConsul { get; private set; } - public string HostName { get; private set; } - public int Port { get; private set; } - } -} \ No newline at end of file diff --git a/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs b/src/Ocelot/ServiceDiscovery/Errors/UnableToFindServiceDiscoveryProviderError.cs similarity index 86% rename from src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs rename to src/Ocelot/ServiceDiscovery/Errors/UnableToFindServiceDiscoveryProviderError.cs index 639e4659..a31ed2ee 100644 --- a/src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs +++ b/src/Ocelot/ServiceDiscovery/Errors/UnableToFindServiceDiscoveryProviderError.cs @@ -1,12 +1,12 @@ -using Ocelot.Errors; - -namespace Ocelot.ServiceDiscovery -{ - public class UnableToFindServiceDiscoveryProviderError : Error - { - public UnableToFindServiceDiscoveryProviderError(string message) - : base(message, OcelotErrorCode.UnableToFindServiceDiscoveryProviderError) - { - } - } -} \ No newline at end of file +using Ocelot.Errors; + +namespace Ocelot.ServiceDiscovery.Errors +{ + public class UnableToFindServiceDiscoveryProviderError : Error + { + public UnableToFindServiceDiscoveryProviderError(string message) + : base(message, OcelotErrorCode.UnableToFindServiceDiscoveryProviderError) + { + } + } +} diff --git a/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs index 9f0bc93a..91e9c700 100644 --- a/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs @@ -1,4 +1,5 @@ using Ocelot.Configuration; +using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.ServiceDiscovery { @@ -6,4 +7,4 @@ namespace Ocelot.ServiceDiscovery { IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute); } -} \ No newline at end of file +} diff --git a/src/Ocelot/ServiceDiscovery/ConfigurationServiceProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/ConfigurationServiceProvider.cs similarity index 88% rename from src/Ocelot/ServiceDiscovery/ConfigurationServiceProvider.cs rename to src/Ocelot/ServiceDiscovery/Providers/ConfigurationServiceProvider.cs index 28a296c5..04369996 100644 --- a/src/Ocelot/ServiceDiscovery/ConfigurationServiceProvider.cs +++ b/src/Ocelot/ServiceDiscovery/Providers/ConfigurationServiceProvider.cs @@ -1,21 +1,21 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Ocelot.Values; - -namespace Ocelot.ServiceDiscovery -{ - public class ConfigurationServiceProvider : IServiceDiscoveryProvider - { - private readonly List _services; - - public ConfigurationServiceProvider(List services) - { - _services = services; - } - - public async Task> Get() - { - return await Task.FromResult(_services); - } - } -} \ No newline at end of file +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery.Providers +{ + public class ConfigurationServiceProvider : IServiceDiscoveryProvider + { + private readonly List _services; + + public ConfigurationServiceProvider(List services) + { + _services = services; + } + + public async Task> Get() + { + return await Task.FromResult(_services); + } + } +} diff --git a/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/ConsulServiceDiscoveryProvider.cs similarity index 63% rename from src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs rename to src/Ocelot/ServiceDiscovery/Providers/ConsulServiceDiscoveryProvider.cs index ef882a50..4c1afcc7 100644 --- a/src/Ocelot/ServiceDiscovery/ConsulServiceDiscoveryProvider.cs +++ b/src/Ocelot/ServiceDiscovery/Providers/ConsulServiceDiscoveryProvider.cs @@ -1,83 +1,82 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Consul; -using Ocelot.Infrastructure.Extensions; -using Ocelot.Logging; -using Ocelot.Values; - -namespace Ocelot.ServiceDiscovery -{ - public class ConsulServiceDiscoveryProvider : IServiceDiscoveryProvider - { - private readonly ConsulRegistryConfiguration _consulConfig; - private readonly IOcelotLogger _logger; - private readonly ConsulClient _consul; - private const string VersionPrefix = "version-"; - - public ConsulServiceDiscoveryProvider(ConsulRegistryConfiguration consulRegistryConfiguration, IOcelotLoggerFactory factory) - {; - _logger = factory.CreateLogger(); - - var consulHost = string.IsNullOrEmpty(consulRegistryConfiguration?.HostName) ? "localhost" : consulRegistryConfiguration.HostName; - - var consulPort = consulRegistryConfiguration?.Port ?? 8500; - - _consulConfig = new ConsulRegistryConfiguration(consulHost, consulPort, consulRegistryConfiguration?.KeyOfServiceInConsul); - - _consul = new ConsulClient(config => - { - config.Address = new Uri($"http://{_consulConfig.HostName}:{_consulConfig.Port}"); - }); - } - - public async Task> Get() - { - var queryResult = await _consul.Health.Service(_consulConfig.KeyOfServiceInConsul, string.Empty, true); - - var services = new List(); - - foreach (var serviceEntry in queryResult.Response) - { - if (IsValid(serviceEntry)) - { - services.Add(BuildService(serviceEntry)); - } - else - { - _logger.LogError($"Unable to use service Address: {serviceEntry.Service.Address} and Port: {serviceEntry.Service.Port} as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); - } - } - - return services.ToList(); - } - - private Service BuildService(ServiceEntry serviceEntry) - { - return new Service( - serviceEntry.Service.Service, - new ServiceHostAndPort(serviceEntry.Service.Address, serviceEntry.Service.Port), - serviceEntry.Service.ID, - GetVersionFromStrings(serviceEntry.Service.Tags), - serviceEntry.Service.Tags ?? Enumerable.Empty()); - } - - private bool IsValid(ServiceEntry serviceEntry) - { - if (serviceEntry.Service.Address.Contains("http://") || serviceEntry.Service.Address.Contains("https://") || serviceEntry.Service.Port <= 0) - { - return false; - } - - return true; - } - - private string GetVersionFromStrings(IEnumerable strings) - { - return strings - ?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) - .TrimStart(VersionPrefix); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Consul; +using Ocelot.Infrastructure.Consul; +using Ocelot.Infrastructure.Extensions; +using Ocelot.Logging; +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery.Providers +{ + public class ConsulServiceDiscoveryProvider : IServiceDiscoveryProvider + { + private readonly ConsulRegistryConfiguration _config; + private readonly IOcelotLogger _logger; + private readonly IConsulClient _consul; + private const string VersionPrefix = "version-"; + + public ConsulServiceDiscoveryProvider(ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory) + {; + _logger = factory.CreateLogger(); + + var consulHost = string.IsNullOrEmpty(config?.Host) ? "localhost" : config.Host; + + var consulPort = config?.Port ?? 8500; + + _config = new ConsulRegistryConfiguration(consulHost, consulPort, config?.KeyOfServiceInConsul, config?.Token); + + _consul = clientFactory.Get(_config); + } + + public async Task> Get() + { + var queryResult = await _consul.Health.Service(_config.KeyOfServiceInConsul, string.Empty, true); + + var services = new List(); + + foreach (var serviceEntry in queryResult.Response) + { + if (IsValid(serviceEntry)) + { + services.Add(BuildService(serviceEntry)); + } + else + { + _logger.LogWarning($"Unable to use service Address: {serviceEntry.Service.Address} and Port: {serviceEntry.Service.Port} as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); + } + } + + return services.ToList(); + } + + private Service BuildService(ServiceEntry serviceEntry) + { + return new Service( + serviceEntry.Service.Service, + new ServiceHostAndPort(serviceEntry.Service.Address, serviceEntry.Service.Port), + serviceEntry.Service.ID, + GetVersionFromStrings(serviceEntry.Service.Tags), + serviceEntry.Service.Tags ?? Enumerable.Empty()); + } + + private bool IsValid(ServiceEntry serviceEntry) + { + if (serviceEntry.Service.Address.Contains("http://") || serviceEntry.Service.Address.Contains("https://") || serviceEntry.Service.Port <= 0) + { + return false; + } + + return true; + } + + private string GetVersionFromStrings(IEnumerable strings) + { + return strings + ?.FirstOrDefault(x => x.StartsWith(VersionPrefix, StringComparison.Ordinal)) + .TrimStart(VersionPrefix); + } + } +} diff --git a/src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs new file mode 100644 index 00000000..ab78db1b --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/Providers/EurekaServiceDiscoveryProvider.cs @@ -0,0 +1,34 @@ +namespace Ocelot.ServiceDiscovery.Providers +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Pivotal.Discovery.Client; + using Values; + + public class EurekaServiceDiscoveryProvider : IServiceDiscoveryProvider + { + private readonly IDiscoveryClient _client; + private readonly string _serviceName; + + public EurekaServiceDiscoveryProvider(string serviceName, IDiscoveryClient client) + { + _client = client; + _serviceName = serviceName; + } + + public Task> Get() + { + var services = new List(); + + var instances = _client.GetInstances(_serviceName); + + if (instances != null && instances.Any()) + { + services.AddRange(instances.Select(i => new Service(i.ServiceId, new ServiceHostAndPort(i.Host, i.Port), "", "", new List()))); + } + + return Task.FromResult(services); + } + } +} diff --git a/src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs b/src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs new file mode 100644 index 00000000..78612148 --- /dev/null +++ b/src/Ocelot/ServiceDiscovery/Providers/FakeEurekaDiscoveryClient.cs @@ -0,0 +1,27 @@ +namespace Ocelot.ServiceDiscovery.Providers +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Pivotal.Discovery.Client; + + public class FakeEurekaDiscoveryClient : IDiscoveryClient + { + public IServiceInstance GetLocalServiceInstance() + { + throw new System.NotImplementedException(); + } + + public IList GetInstances(string serviceId) + { + throw new System.NotImplementedException(); + } + + public Task ShutdownAsync() + { + throw new System.NotImplementedException(); + } + + public string Description { get; } + public IList Services { get; } + } +} diff --git a/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/IServiceDiscoveryProvider.cs similarity index 79% rename from src/Ocelot/ServiceDiscovery/IServiceDiscoveryProvider.cs rename to src/Ocelot/ServiceDiscovery/Providers/IServiceDiscoveryProvider.cs index 3d9887f3..ef28375e 100644 --- a/src/Ocelot/ServiceDiscovery/IServiceDiscoveryProvider.cs +++ b/src/Ocelot/ServiceDiscovery/Providers/IServiceDiscoveryProvider.cs @@ -1,11 +1,11 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Ocelot.Values; - -namespace Ocelot.ServiceDiscovery -{ - public interface IServiceDiscoveryProvider - { - Task> Get(); - } -} \ No newline at end of file +using System.Collections.Generic; +using System.Threading.Tasks; +using Ocelot.Values; + +namespace Ocelot.ServiceDiscovery.Providers +{ + public interface IServiceDiscoveryProvider + { + Task> Get(); + } +} diff --git a/src/Ocelot/ServiceDiscovery/ServiceFabricServiceDiscoveryProvider.cs b/src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs similarity index 90% rename from src/Ocelot/ServiceDiscovery/ServiceFabricServiceDiscoveryProvider.cs rename to src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs index 257298b7..bc7ebf6e 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceFabricServiceDiscoveryProvider.cs +++ b/src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Ocelot.ServiceDiscovery.Configuration; using Ocelot.Values; -namespace Ocelot.ServiceDiscovery +namespace Ocelot.ServiceDiscovery.Providers { public class ServiceFabricServiceDiscoveryProvider : IServiceDiscoveryProvider { diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index 86b28a72..cd678c4f 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -1,17 +1,26 @@ using System.Collections.Generic; using Ocelot.Configuration; +using Ocelot.Infrastructure.Consul; using Ocelot.Logging; +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.ServiceDiscovery { + using Pivotal.Discovery.Client; + public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory { private readonly IOcelotLoggerFactory _factory; + private readonly IConsulClientFactory _consulFactory; + private readonly IDiscoveryClient _eurekaClient; - public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory) + public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IConsulClientFactory consulFactory, IDiscoveryClient eurekaClient) { _factory = factory; + _consulFactory = consulFactory; + _eurekaClient = eurekaClient; } public IServiceDiscoveryProvider Get(ServiceProviderConfiguration serviceConfig, DownstreamReRoute reRoute) @@ -35,14 +44,19 @@ namespace Ocelot.ServiceDiscovery private IServiceDiscoveryProvider GetServiceDiscoveryProvider(ServiceProviderConfiguration serviceConfig, string serviceName) { - if (serviceConfig.Type == "ServiceFabric") + if (serviceConfig.Type?.ToLower() == "servicefabric") { var config = new ServiceFabricConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName); return new ServiceFabricServiceDiscoveryProvider(config); } - var consulRegistryConfiguration = new ConsulRegistryConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName); - return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration, _factory); + if (serviceConfig.Type?.ToLower() == "eureka") + { + return new EurekaServiceDiscoveryProvider(serviceName, _eurekaClient); + } + + var consulRegistryConfiguration = new ConsulRegistryConfiguration(serviceConfig.Host, serviceConfig.Port, serviceName, serviceConfig.Token); + return new ConsulServiceDiscoveryProvider(consulRegistryConfiguration, _factory, _consulFactory); } } } diff --git a/src/Ocelot/Values/DownstreamPath.cs b/src/Ocelot/Values/DownstreamPath.cs index b4dd346b..f0821118 100644 --- a/src/Ocelot/Values/DownstreamPath.cs +++ b/src/Ocelot/Values/DownstreamPath.cs @@ -7,6 +7,6 @@ Value = value; } - public string Value { get; private set; } + public string Value { get; } } } diff --git a/src/Ocelot/Values/DownstreamUrl.cs b/src/Ocelot/Values/DownstreamUrl.cs deleted file mode 100644 index 48644595..00000000 --- a/src/Ocelot/Values/DownstreamUrl.cs +++ /dev/null @@ -1,12 +0,0 @@ -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/PathTemplate.cs b/src/Ocelot/Values/PathTemplate.cs index 6d7221a6..584f80ac 100644 --- a/src/Ocelot/Values/PathTemplate.cs +++ b/src/Ocelot/Values/PathTemplate.cs @@ -7,6 +7,6 @@ Value = value; } - public string Value { get; private set; } + public string Value { get; } } } diff --git a/src/Ocelot/Values/Service.cs b/src/Ocelot/Values/Service.cs index abf5449b..234b248b 100644 --- a/src/Ocelot/Values/Service.cs +++ b/src/Ocelot/Values/Service.cs @@ -5,9 +5,9 @@ namespace Ocelot.Values public class Service { public Service(string name, - ServiceHostAndPort hostAndPort, - string id, - string version, + ServiceHostAndPort hostAndPort, + string id, + string version, IEnumerable tags) { Name = name; @@ -17,14 +17,14 @@ namespace Ocelot.Values Tags = tags; } - public string Id { get; private set; } + public string Id { get; } - public string Name { get; private set; } + public string Name { get; } - public string Version { get; private set; } + public string Version { get; } - public IEnumerable Tags { get; private set; } + public IEnumerable Tags { get; } - public ServiceHostAndPort HostAndPort { get; private set; } + public ServiceHostAndPort HostAndPort { get; } } } diff --git a/src/Ocelot/Values/ServiceHostAndPort.cs b/src/Ocelot/Values/ServiceHostAndPort.cs index 135944b1..4e4271b8 100644 --- a/src/Ocelot/Values/ServiceHostAndPort.cs +++ b/src/Ocelot/Values/ServiceHostAndPort.cs @@ -8,7 +8,8 @@ DownstreamPort = downstreamPort; } - public string DownstreamHost { get; private set; } - public int DownstreamPort { get; private set; } + public string DownstreamHost { get; } + + public int DownstreamPort { get; } } } diff --git a/src/Ocelot/Values/UpstreamPathTemplate.cs b/src/Ocelot/Values/UpstreamPathTemplate.cs index 0cd44bc5..4b33a230 100644 --- a/src/Ocelot/Values/UpstreamPathTemplate.cs +++ b/src/Ocelot/Values/UpstreamPathTemplate.cs @@ -8,7 +8,8 @@ namespace Ocelot.Values Priority = priority; } - public string Template {get;} - public int Priority {get;} + public string Template { get; } + + public int Priority { get; } } -} \ No newline at end of file +} diff --git a/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddleware.cs b/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddleware.cs new file mode 100644 index 00000000..68b2df48 --- /dev/null +++ b/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddleware.cs @@ -0,0 +1,102 @@ +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.WebSockets.Middleware +{ + public class WebSocketsProxyMiddleware : OcelotMiddleware + { + private OcelotRequestDelegate _next; + + public WebSocketsProxyMiddleware(OcelotRequestDelegate next, + IOcelotLoggerFactory loggerFactory) + :base(loggerFactory.CreateLogger()) + { + _next = next; + } + + public async Task Invoke(DownstreamContext context) + { + await Proxy(context.HttpContext, context.DownstreamRequest.ToUri()); + } + + private async Task Proxy(HttpContext context, string serverEndpoint) + { + var wsToUpstreamClient = await context.WebSockets.AcceptWebSocketAsync(); + + var wsToDownstreamService = new ClientWebSocket(); + var uri = new Uri(serverEndpoint); + await wsToDownstreamService.ConnectAsync(uri, CancellationToken.None); + + var receiveFromUpstreamSendToDownstream = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + var receiveSegment = new ArraySegment(buffer); + + while (wsToUpstreamClient.State == WebSocketState.Open || wsToUpstreamClient.State == WebSocketState.CloseSent) + { + var result = await wsToUpstreamClient.ReceiveAsync(receiveSegment, CancellationToken.None); + + var sendSegment = new ArraySegment(buffer, 0, result.Count); + + if(result.MessageType == WebSocketMessageType.Close) + { + await wsToUpstreamClient.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", + CancellationToken.None); + + await wsToDownstreamService.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", + CancellationToken.None); + + break; + } + + await wsToDownstreamService.SendAsync(sendSegment, result.MessageType, result.EndOfMessage, + CancellationToken.None); + + if (wsToUpstreamClient.State != WebSocketState.Open) + { + await wsToDownstreamService.CloseAsync(WebSocketCloseStatus.Empty, "", + CancellationToken.None); + break; + } + } + }); + + var receiveFromDownstreamAndSendToUpstream = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (wsToDownstreamService.State == WebSocketState.Open || wsToDownstreamService.State == WebSocketState.CloseSent) + { + if (wsToUpstreamClient.State != WebSocketState.Open) + { + break; + } + else + { + var receiveSegment = new ArraySegment(buffer); + var result = await wsToDownstreamService.ReceiveAsync(receiveSegment, CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Close) + { + break; + } + + var sendSegment = new ArraySegment(buffer, 0, result.Count); + + //send to upstream client + await wsToUpstreamClient.SendAsync(sendSegment, result.MessageType, result.EndOfMessage, + CancellationToken.None); + } + } + }); + + await Task.WhenAll(receiveFromDownstreamAndSendToUpstream, receiveFromUpstreamSendToDownstream); + } + } +} diff --git a/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddlewareExtensions.cs b/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddlewareExtensions.cs new file mode 100644 index 00000000..e973dfc3 --- /dev/null +++ b/src/Ocelot/WebSockets/Middleware/WebSocketsProxyMiddlewareExtensions.cs @@ -0,0 +1,12 @@ +using Ocelot.Middleware.Pipeline; + +namespace Ocelot.WebSockets.Middleware +{ + public static class WebSocketsProxyMiddlewareExtensions + { + public static IOcelotPipelineBuilder UseWebSocketsProxyMiddleware(this IOcelotPipelineBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index 3944adfc..8e0e5dfd 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -1,11 +1,17 @@ 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.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; using Shouldly; using TestStack.BDDfy; using Xunit; @@ -15,6 +21,7 @@ namespace Ocelot.AcceptanceTests public class AggregateTests : IDisposable { private IWebHost _serviceOneBuilder; + private IWebHost _serviceTwoBuilder; private readonly Steps _steps; private string _downstreamPathOne; private string _downstreamPathTwo; @@ -24,6 +31,75 @@ namespace Ocelot.AcceptanceTests _steps = new Steps(); } + [Fact] + public void should_return_response_200_with_simple_url_user_defined_aggregate() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51885, + } + }, + UpstreamPathTemplate = "/laura", + UpstreamHttpMethod = new List { "Get" }, + Key = "Laura" + }, + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51886, + } + }, + UpstreamPathTemplate = "/tom", + UpstreamHttpMethod = new List { "Get" }, + Key = "Tom" + } + }, + Aggregates = new List + { + new FileAggregateReRoute + { + UpstreamPathTemplate = "/", + UpstreamHost = "localhost", + ReRouteKeys = new List + { + "Tom", + "Laura" + }, + Aggregator = "FakeDefinedAggregator" + } + } + }; + + var expected = "Bye from Laura, Bye from Tom"; + + this.Given(x => x.GivenServiceOneIsRunning("http://localhost:51885", "/", 200, "{Hello from Laura}")) + .Given(x => x.GivenServiceTwoIsRunning("http://localhost:51886", "/", 200, "{Hello from Tom}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithSpecficAggregatorsRegisteredInDi()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) + .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) + .BDDfy(); + } + [Fact] public void should_return_response_200_with_simple_url() { @@ -325,7 +401,7 @@ namespace Ocelot.AcceptanceTests private void GivenServiceTwoIsRunning(string baseUrl, string basePath, int statusCode, string responseBody) { - _serviceOneBuilder = new WebHostBuilder() + _serviceTwoBuilder = new WebHostBuilder() .UseUrls(baseUrl) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) @@ -351,7 +427,7 @@ namespace Ocelot.AcceptanceTests }) .Build(); - _serviceOneBuilder.Start(); + _serviceTwoBuilder.Start(); } internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) @@ -363,7 +439,33 @@ namespace Ocelot.AcceptanceTests public void Dispose() { _serviceOneBuilder?.Dispose(); + _serviceTwoBuilder?.Dispose(); _steps.Dispose(); } } + + public class FakeDepdendency + { + } + + public class FakeDefinedAggregator : IDefinedAggregator + { + private readonly FakeDepdendency _dep; + + public FakeDefinedAggregator(FakeDepdendency dep) + { + _dep = dep; + } + + public async Task Aggregate(List responses) + { + var one = await responses[0].Content.ReadAsStringAsync(); + var two = await responses[1].Content.ReadAsStringAsync(); + + var merge = $"{one}, {two}"; + merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); + var headers = responses.SelectMany(x => x.Headers).ToList(); + return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers); + } + } } diff --git a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs index a7411ebe..51f459ef 100644 --- a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs @@ -14,6 +14,8 @@ using static Rafty.Infrastructure.Wait; namespace Ocelot.AcceptanceTests { + using Xunit.Abstractions; + public class ButterflyTracingTests : IDisposable { private IWebHost _serviceOneBuilder; @@ -23,9 +25,11 @@ namespace Ocelot.AcceptanceTests private string _downstreamPathOne; private string _downstreamPathTwo; private int _butterflyCalled; + private readonly ITestOutputHelper _output; - public ButterflyTracingTests() + public ButterflyTracingTests(ITestOutputHelper output) { + _output = output; _steps = new Steps(); } @@ -70,7 +74,7 @@ namespace Ocelot.AcceptanceTests new FileHostAndPort { Host = "localhost", - Port = 51888, + Port = 51388, } }, UpstreamPathTemplate = "/api002/values", @@ -91,24 +95,80 @@ namespace Ocelot.AcceptanceTests var butterflyUrl = "http://localhost:9618"; - this.Given(x => GivenServiceOneIsRunning("http://localhost:51887", "/api/values", 200, "Hello from Laura", butterflyUrl)) - .And(x => GivenServiceTwoIsRunning("http://localhost:51888", "/api/values", 200, "Hello from Tom", butterflyUrl)) - .And(x => GivenFakeButterfly(butterflyUrl)) + this.Given(x => GivenFakeButterfly(butterflyUrl)) + .And(x => GivenServiceOneIsRunning("http://localhost:51887", "/api/values", 200, "Hello from Laura", butterflyUrl)) + .And(x => GivenServiceTwoIsRunning("http://localhost:51388", "/api/values", 200, "Hello from Tom", butterflyUrl)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api002/values")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api002/values")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) .BDDfy(); - var commandOnAllStateMachines = WaitFor(5000).Until(() => _butterflyCalled == 4); + var commandOnAllStateMachines = WaitFor(10000).Until(() => _butterflyCalled >= 4); + + _output.WriteLine($"_butterflyCalled is {_butterflyCalled}"); commandOnAllStateMachines.ShouldBeTrue(); } + [Fact] + public void should_return_tracing_header() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/values", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51387, + } + }, + UpstreamPathTemplate = "/api001/values", + UpstreamHttpMethod = new List { "Get" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true + }, + QoSOptions = new FileQoSOptions + { + ExceptionsAllowedBeforeBreaking = 3, + DurationOfBreak = 10, + TimeoutValue = 5000 + }, + DownstreamHeaderTransform = new Dictionary() + { + {"Trace-Id", "{TraceId}"}, + {"Tom", "Laura"} + } + } + } + }; + + var butterflyUrl = "http://localhost:9618"; + + this.Given(x => GivenFakeButterfly(butterflyUrl)) + .And(x => GivenServiceOneIsRunning("http://localhost:51387", "/api/values", 200, "Hello from Laura", butterflyUrl)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningUsingButterfly(butterflyUrl)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api001/values")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.ThenTheTraceHeaderIsSet("Trace-Id")) + .And(x => _steps.ThenTheResponseHeaderIs("Tom", "Laura")) + .BDDfy(); + } + private void GivenServiceOneIsRunning(string baseUrl, string basePath, int statusCode, string responseBody, string butterflyUrl) { _serviceOneBuilder = new WebHostBuilder() diff --git a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs index f73a360e..1b9469e1 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs @@ -19,6 +19,7 @@ namespace Ocelot.AcceptanceTests { using IdentityServer4; using IdentityServer4.Test; + using Shouldly; public class ClaimsToQueryStringForwardingTests : IDisposable { @@ -27,6 +28,7 @@ namespace Ocelot.AcceptanceTests private readonly Steps _steps; private Action _options; private string _identityServerRootUrl = "http://localhost:57888"; + private string _downstreamQueryString; public ClaimsToQueryStringForwardingTests() { @@ -105,6 +107,71 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_return_response_200_and_foward_claim_as_query_string_and_preserve_original_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 = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 57876, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + AuthenticationOptions = new FileAuthenticationOptions + { + AuthenticationProviderKey = "Test", + AllowedScopes = new List + { + "openid", "offline_access", "api" + }, + }, + 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(_options, "Test")) + .And(x => _steps.GivenIHaveAddedATokenToMyRequest()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/?test=1&test=2")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("CustomerId: 123 LocationId: 1 UserType: registered UserId: 1231231")) + .And(_ => _downstreamQueryString.ShouldBe("?test=1&test=2&CustomerId=123&LocationId=1&UserId=1231231&UserType=registered")) + .BDDfy(); + } + private void GivenThereIsAServiceRunningOn(string url, int statusCode) { _servicebuilder = new WebHostBuilder() @@ -117,6 +184,8 @@ namespace Ocelot.AcceptanceTests { app.Run(async context => { + _downstreamQueryString = context.Request.QueryString.Value; + StringValues customerId; context.Request.Query.TryGetValue("CustomerId", out customerId); diff --git a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs index 7c9ac239..5a5cce82 100644 --- a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs @@ -9,8 +9,10 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Configuration.File; +using Shouldly; using TestStack.BDDfy; using Xunit; +using static Ocelot.Infrastructure.Wait; namespace Ocelot.AcceptanceTests { @@ -261,21 +263,27 @@ namespace Ocelot.AcceptanceTests .And(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status")) .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => GivenTheConsulConfigurationIs(secondConsulConfig)) - .And(x => GivenIWaitForTheConfigToReplicateToOcelot()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) + .Then(x => ThenTheConfigIsUpdatedInOcelot()) .BDDfy(); } - private void GivenIWaitForTheConfigToReplicateToOcelot() + private void ThenTheConfigIsUpdatedInOcelot() { - var stopWatch = Stopwatch.StartNew(); - while (stopWatch.ElapsedMilliseconds < 10000) - { - //do nothing! - } + var result = WaitFor(20000).Until(() => { + try + { + _steps.WhenIGetUrlOnTheApiGateway("/cs/status/awesome"); + _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK); + _steps.ThenTheResponseBodyShouldBe("Hello from Laura"); + return true; + } + catch (Exception) + { + return false; + } + }); + result.ShouldBeTrue(); } private void GivenTheConsulConfigurationIs(FileConfiguration config) @@ -295,7 +303,7 @@ namespace Ocelot.AcceptanceTests { app.Run(async context => { - if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/OcelotConfiguration") + if (context.Request.Method.ToLower() == "get" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") { var json = JsonConvert.SerializeObject(_config); @@ -307,7 +315,7 @@ namespace Ocelot.AcceptanceTests await context.Response.WriteJsonAsync(new FakeConsulGetResponse[] { kvp }); } - else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/OcelotConfiguration") + else if (context.Request.Method.ToLower() == "put" && context.Request.Path.Value == "/v1/kv/InternalConfiguration") { try { @@ -344,7 +352,7 @@ namespace Ocelot.AcceptanceTests public int CreateIndex => 100; public int ModifyIndex => 200; public int LockIndex => 200; - public string Key => "OcelotConfiguration"; + public string Key => "InternalConfiguration"; public int Flags => 0; public string Value { get; private set; } public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; diff --git a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs index 341986fe..422348d0 100644 --- a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs +++ b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs @@ -26,7 +26,7 @@ namespace Ocelot.AcceptanceTests { _counter = 0; _steps = new Steps(); - _configurationPath = "configuration.json"; + _configurationPath = "ocelot.json"; } [Fact] diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs index 350e1e52..b964161a 100644 --- a/test/Ocelot.AcceptanceTests/HeaderTests.cs +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -16,6 +16,7 @@ namespace Ocelot.AcceptanceTests public class HeaderTests : IDisposable { private IWebHost _builder; + private int _count; private readonly Steps _steps; public HeaderTests() @@ -184,6 +185,126 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void request_should_reuse_cookies_with_cookie_container() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/sso/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6774, + } + }, + UpstreamPathTemplate = "/sso/{everything}", + UpstreamHttpMethod = new List { "Get", "Post", "Options" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseCookieContainer = true + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6774", "/sso/test", 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) + .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void request_should_have_own_cookies_no_cookie_container() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/sso/{everything}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 6775, + } + }, + UpstreamPathTemplate = "/sso/{everything}", + UpstreamHttpMethod = new List { "Get", "Post", "Options" }, + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseCookieContainer = false + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:6775", "/sso/test", 200)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) + .And(x => _steps.GivenIAddCookieToMyRequest("test=1; path=/")) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/sso/test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(context => + { + if (_count == 0) + { + context.Response.Cookies.Append("test", "0"); + _count++; + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + } + + if (context.Request.Cookies.TryGetValue("test", out var cookieValue) || context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) + { + if (cookieValue == "0" || headerValue == "test=1; path=/") + { + context.Response.StatusCode = statusCode; + return Task.CompletedTask; + } + } + + context.Response.StatusCode = 500; + return Task.CompletedTask; + }); + }) + .Build(); + + _builder.Start(); + } + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string headerKey) { _builder = new WebHostBuilder() diff --git a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs index d67e7e41..14c69f9a 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net; -using Consul; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; @@ -28,10 +26,10 @@ namespace Ocelot.AcceptanceTests } [Fact] - public void should_use_service_discovery_and_load_balance_request() + public void should_load_balance_request() { var downstreamServiceOneUrl = "http://localhost:50881"; - var downstreamServiceTwoUrl = "http://localhost:50882"; + var downstreamServiceTwoUrl = "http://localhost:50892"; var configuration = new FileConfiguration { @@ -54,7 +52,7 @@ namespace Ocelot.AcceptanceTests new FileHostAndPort { Host = "localhost", - Port = 50882 + Port = 50892 } } } @@ -74,18 +72,6 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } - private void ThenOnlyOneServiceHasBeenCalled() - { - _counterOne.ShouldBe(10); - _counterTwo.ShouldBe(0); - } - - private void GivenIResetCounters() - { - _counterOne = 0; - _counterTwo = 0; - } - private void ThenBothServicesCalledRealisticAmountOfTimes(int bottom, int top) { _counterOne.ShouldBeInRange(bottom, top); @@ -121,7 +107,7 @@ namespace Ocelot.AcceptanceTests context.Response.StatusCode = statusCode; await context.Response.WriteAsync(response); } - catch (System.Exception exception) + catch (Exception exception) { await context.Response.WriteAsync(exception.StackTrace); } diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index ed09ed96..9edbfd74 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -15,7 +15,13 @@ - + + PreserveNewest + + + PreserveNewest + + PreserveNewest @@ -27,32 +33,27 @@ - - - - + + + + all - - - - - - - - + + + + + + + + - + - + - - - ..\..\..\..\Users\TGP\.nuget\packages\castle.core\4.2.1\lib\netstandard1.3\Castle.Core.dll - - diff --git a/test/Ocelot.AcceptanceTests/QoSTests.cs b/test/Ocelot.AcceptanceTests/QoSTests.cs index d11721b8..0a9a110a 100644 --- a/test/Ocelot.AcceptanceTests/QoSTests.cs +++ b/test/Ocelot.AcceptanceTests/QoSTests.cs @@ -25,6 +25,82 @@ namespace Ocelot.AcceptanceTests _steps = new Steps(); } + [Fact] + public void should_not_timeout() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51569, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Post" }, + QoSOptions = new FileQoSOptions + { + TimeoutValue = 1000, + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51569", 200, string.Empty, 10)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void should_timeout() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51579, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Post" }, + QoSOptions = new FileQoSOptions + { + TimeoutValue = 10, + } + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51579", 201, string.Empty, 1000)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } + [Fact] public void should_open_circuit_breaker_then_close() { @@ -41,7 +117,7 @@ namespace Ocelot.AcceptanceTests new FileHostAndPort { Host = "localhost", - Port = 51872, + Port = 51892, } }, UpstreamPathTemplate = "/", @@ -56,7 +132,7 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51872", "Hello from Laura")) + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51892", "Hello from Laura")) .Given(x => _steps.GivenThereIsAConfiguration(configuration)) .Given(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -122,7 +198,7 @@ namespace Ocelot.AcceptanceTests }; this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn("http://localhost:51872", "Hello from Laura")) - .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", 200, "Hello from Tom")) + .And(x => x.GivenThereIsAServiceRunningOn("http://localhost:51880/", 200, "Hello from Tom", 0)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) @@ -193,7 +269,7 @@ namespace Ocelot.AcceptanceTests _brokenService.Start(); } - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody) + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) { _workingService = new WebHostBuilder() .UseUrls(url) @@ -205,6 +281,7 @@ namespace Ocelot.AcceptanceTests { app.Run(async context => { + Thread.Sleep(timeout); context.Response.StatusCode = statusCode; await context.Response.WriteAsync(responseBody); }); diff --git a/test/Ocelot.AcceptanceTests/RequestIdTests.cs b/test/Ocelot.AcceptanceTests/RequestIdTests.cs index 6ac271c9..7989dc90 100644 --- a/test/Ocelot.AcceptanceTests/RequestIdTests.cs +++ b/test/Ocelot.AcceptanceTests/RequestIdTests.cs @@ -132,6 +132,43 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_use_global_request_id_create_and_forward() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51873, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + RequestIdKey = _steps.RequestIdKey + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51873")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheRequestIdIsReturned()) + .BDDfy(); + } + private void GivenThereIsAServiceRunningOn(string url) { _builder = new WebHostBuilder() diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 0a16e565..6ede8524 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -872,6 +872,105 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } + [Fact] + public void should_use_priority() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/goods/{url}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/goods/{url}", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 53879, + } + }, + Priority = 0 + }, + new FileReRoute + { + DownstreamPathTemplate = "/goods/delete", + DownstreamScheme = "http", + UpstreamPathTemplate = "/goods/delete", + UpstreamHttpMethod = new List { "Get" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 52879, + } + }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:52879/", "/goods/delete", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/goods/delete")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_fix_issue_271() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/v1/{everything}", + DownstreamScheme = "http", + UpstreamPathTemplate = "/api/v1/{everything}", + UpstreamHttpMethod = new List { "Get", "Put", "Post" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 54879, + } + }, + }, + new FileReRoute + { + DownstreamPathTemplate = "/connect/token", + DownstreamScheme = "http", + UpstreamPathTemplate = "/connect/token", + UpstreamHttpMethod = new List { "Post" }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 5001, + } + }, + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:54879/", "/api/v1/modules/Test", 200, "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/api/v1/modules/Test")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) { _builder = new WebHostBuilder() diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index 97e104c4..151bb99c 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -1,39 +1,92 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using Consul; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Ocelot.Configuration.File; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - namespace Ocelot.AcceptanceTests { + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using Consul; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Http; + using Ocelot.Configuration.File; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + using Newtonsoft.Json; + using Pivotal.Discovery.Client; + public class ServiceDiscoveryTests : IDisposable { private IWebHost _builderOne; private IWebHost _builderTwo; private IWebHost _fakeConsulBuilder; private readonly Steps _steps; - private readonly List _serviceEntries; + private readonly List _consulServices; + private readonly List _eurekaInstances; private int _counterOne; private int _counterTwo; - private static readonly object _syncLock = new object(); + private static readonly object SyncLock = new object(); private IWebHost _builder; private string _downstreamPath; + private string _receivedToken; public ServiceDiscoveryTests() { _steps = new Steps(); - _serviceEntries = new List(); + _consulServices = new List(); + _eurekaInstances = new List(); } [Fact] - public void should_use_service_discovery_and_load_balance_request() + public void should_use_eureka_service_discovery_and_make_request() + { + var eurekaPort = 8761; + var serviceName = "product"; + var downstreamServicePort = 50371; + var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; + var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; + + var instanceOne = new FakeEurekaService(serviceName, "localhost", downstreamServicePort, false, + new Uri($"http://localhost:{downstreamServicePort}"), new Dictionary()); + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + ServiceName = serviceName, + LoadBalancer = "LeastConnection", + UseServiceDiscovery = true, + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() + { + Type = "Eureka" + } + } + }; + + this.Given(x => x.GivenEurekaProductServiceOneIsRunning(downstreamServiceOneUrl, 200)) + .And(x => x.GivenThereIsAFakeEurekaServiceDiscoveryProvider(fakeEurekaServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithEureka(instanceOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe(nameof(ServiceDiscoveryTests))) + .BDDfy(); + } + + [Fact] + public void should_use_consul_service_discovery_and_load_balance_request() { var consulPort = 8502; var serviceName = "product"; @@ -100,13 +153,12 @@ namespace Ocelot.AcceptanceTests .BDDfy(); } - //test from issue 213 [Fact] public void should_handle_request_to_consul_for_downstream_service_and_make_request() { - var consulPort = 8505; - var serviceName = "web"; - var downstreamServiceOneUrl = "http://localhost:8080"; + const int consulPort = 8505; + const string serviceName = "web"; + const string downstreamServiceOneUrl = "http://localhost:8080"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry() { @@ -116,7 +168,7 @@ namespace Ocelot.AcceptanceTests Address = "localhost", Port = 8080, ID = "web_90_0_2_224_8080", - Tags = new string[1]{"version-v1"} + Tags = new[] {"version-v1"} }, }; @@ -145,19 +197,77 @@ namespace Ocelot.AcceptanceTests } }; - this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) - .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) - .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunning()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/home")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void should_use_token_to_make_request_to_consul() + { + var token = "abctoken"; + var consulPort = 8515; + var serviceName = "web"; + var downstreamServiceOneUrl = "http://localhost:8081"; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = serviceName, + Address = "localhost", + Port = 8081, + ID = "web_90_0_2_224_8080", + Tags = new[] { "version-v1" } + }, + }; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/api/home", + DownstreamScheme = "http", + UpstreamPathTemplate = "/home", + UpstreamHttpMethod = new List { "Get", "Options" }, + ServiceName = serviceName, + LoadBalancer = "LeastConnection", + UseServiceDiscovery = true, + } + }, + GlobalConfiguration = new FileGlobalConfiguration() + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider() + { + Host = "localhost", + Port = consulPort, + Token = token + } + } + }; + + this.Given(_ => GivenThereIsAServiceRunningOn(downstreamServiceOneUrl, "/api/home", 200, "Hello from Laura")) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .And(_ => _steps.GivenThereIsAConfiguration(configuration)) + .And(_ => _steps.GivenOcelotIsRunning()) + .When(_ => _steps.WhenIGetUrlOnTheApiGateway("/home")) + .Then(_ => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(_ => _receivedToken.ShouldBe(token)) .BDDfy(); } [Fact] - public void should_send_request_to_service_after_it_becomes_available() + public void should_send_request_to_service_after_it_becomes_available_in_consul() { var consulPort = 8501; var serviceName = "product"; @@ -235,7 +345,7 @@ namespace Ocelot.AcceptanceTests private void WhenIAddAServiceBackIn(ServiceEntry serviceEntryTwo) { - _serviceEntries.Add(serviceEntryTwo); + _consulServices.Add(serviceEntryTwo); } private void ThenOnlyOneServiceHasBeenCalled() @@ -246,7 +356,7 @@ namespace Ocelot.AcceptanceTests private void WhenIRemoveAService(ServiceEntry serviceEntryTwo) { - _serviceEntries.Remove(serviceEntryTwo); + _consulServices.Remove(serviceEntryTwo); } private void GivenIResetCounters() @@ -271,10 +381,100 @@ namespace Ocelot.AcceptanceTests { foreach(var serviceEntry in serviceEntries) { - _serviceEntries.Add(serviceEntry); + _consulServices.Add(serviceEntry); } } + private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) + { + foreach (var instance in serviceInstances) + { + _eurekaInstances.Add(instance); + } + } + + private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string serviceName) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Path.Value == "/eureka/apps/") + { + var apps = new List(); + + foreach (var serviceInstance in _eurekaInstances) + { + var a = new Application + { + name = serviceName, + instance = new List + { + new Instance + { + instanceId = $"{serviceInstance.Host}:{serviceInstance}", + hostName = serviceInstance.Host, + app = serviceName, + ipAddr = "127.0.0.1", + status = "UP", + overriddenstatus = "UNKNOWN", + port = new Port {value = serviceInstance.Port, enabled = "true"}, + securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, + countryId = 1, + dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, + leaseInfo = new LeaseInfo + { + renewalIntervalInSecs = 30, + durationInSecs = 90, + registrationTimestamp = 1457714988223, + lastRenewalTimestamp= 1457716158319, + evictionTimestamp = 0, + serviceUpTimestamp = 1457714988223 + }, + metadata = new Metadata + { + value = "java.util.Collections$EmptyMap" + }, + homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", + vipAddress = serviceName, + isCoordinatingDiscoveryServer = "false", + lastUpdatedTimestamp = "1457714988223", + lastDirtyTimestamp = "1457714988172", + actionType = "ADDED" + } + } + }; + + apps.Add(a); + } + + var applications = new EurekaApplications + { + applications = new Applications + { + application = apps, + apps__hashcode = "UP_1_", + versions__delta = "1" + } + }; + + await context.Response.WriteJsonAsync(applications); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) { _fakeConsulBuilder = new WebHostBuilder() @@ -289,7 +489,12 @@ namespace Ocelot.AcceptanceTests { if(context.Request.Path.Value == $"/v1/health/service/{serviceName}") { - await context.Response.WriteJsonAsync(_serviceEntries); + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) + { + _receivedToken = values.First(); + } + + await context.Response.WriteJsonAsync(_consulServices); } }); }) @@ -312,8 +517,8 @@ namespace Ocelot.AcceptanceTests { try { - var response = string.Empty; - lock (_syncLock) + string response; + lock (SyncLock) { _counterOne++; response = _counterOne.ToString(); @@ -321,7 +526,7 @@ namespace Ocelot.AcceptanceTests context.Response.StatusCode = statusCode; await context.Response.WriteAsync(response); } - catch (System.Exception exception) + catch (Exception exception) { await context.Response.WriteAsync(exception.StackTrace); } @@ -346,8 +551,8 @@ namespace Ocelot.AcceptanceTests { try { - var response = string.Empty; - lock (_syncLock) + string response; + lock (SyncLock) { _counterTwo++; response = _counterTwo.ToString(); @@ -356,7 +561,7 @@ namespace Ocelot.AcceptanceTests context.Response.StatusCode = statusCode; await context.Response.WriteAsync(response); } - catch (System.Exception exception) + catch (Exception exception) { await context.Response.WriteAsync(exception.StackTrace); } @@ -367,6 +572,34 @@ namespace Ocelot.AcceptanceTests _builderTwo.Start(); } + private void GivenEurekaProductServiceOneIsRunning(string url, int statusCode) + { + _builderOne = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + try + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(nameof(ServiceDiscoveryTests)); + } + catch (Exception exception) + { + await context.Response.WriteAsync(exception.StackTrace); + } + }); + }) + .Build(); + + _builderOne.Start(); + } + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) { _builder = new WebHostBuilder() @@ -405,4 +638,113 @@ namespace Ocelot.AcceptanceTests _steps.Dispose(); } } + + public class FakeEurekaService : IServiceInstance + { + public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) + { + ServiceId = serviceId; + Host = host; + Port = port; + IsSecure = isSecure; + Uri = uri; + Metadata = metadata; + } + + public string ServiceId { get; } + public string Host { get; } + public int Port { get; } + public bool IsSecure { get; } + public Uri Uri { get; } + public IDictionary Metadata { get; } + } + + public class Port + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class SecurePort + { + [JsonProperty("$")] + public int value { get; set; } + + [JsonProperty("@enabled")] + public string enabled { get; set; } + } + + public class DataCenterInfo + { + [JsonProperty("@class")] + public string value { get; set; } + + public string name { get; set; } + } + + public class LeaseInfo + { + public int renewalIntervalInSecs { get; set; } + + public int durationInSecs { get; set; } + + public long registrationTimestamp { get; set; } + + public long lastRenewalTimestamp { get; set; } + + public int evictionTimestamp { get; set; } + + public long serviceUpTimestamp { get; set; } + } + + public class Metadata + { + [JsonProperty("@class")] + public string value { get; set; } + } + + public class Instance + { + public string instanceId { get; set; } + public string hostName { get; set; } + public string app { get; set; } + public string ipAddr { get; set; } + public string status { get; set; } + public string overriddenstatus { get; set; } + public Port port { get; set; } + public SecurePort securePort { get; set; } + public int countryId { get; set; } + public DataCenterInfo dataCenterInfo { get; set; } + public LeaseInfo leaseInfo { get; set; } + public Metadata metadata { get; set; } + public string homePageUrl { get; set; } + public string statusPageUrl { get; set; } + public string healthCheckUrl { get; set; } + public string vipAddress { get; set; } + public string isCoordinatingDiscoveryServer { get; set; } + public string lastUpdatedTimestamp { get; set; } + public string lastDirtyTimestamp { get; set; } + public string actionType { get; set; } + } + + public class Application + { + public string name { get; set; } + public List instance { get; set; } + } + + public class Applications + { + public string versions__delta { get; set; } + public string apps__hashcode { get; set; } + public List application { get; set; } + } + + public class EurekaApplications + { + public Applications applications { get; set; } + } } diff --git a/test/Ocelot.AcceptanceTests/SslTests.cs b/test/Ocelot.AcceptanceTests/SslTests.cs new file mode 100644 index 00000000..309bbf28 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/SslTests.cs @@ -0,0 +1,146 @@ +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 Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class SslTests : IDisposable + { + private IWebHost _builder; + private readonly Steps _steps; + private string _downstreamPath; + + public SslTests() + { + _steps = new Steps(); + } + + [Fact] + public void should_dangerous_accept_any_server_certificate_validator() + { + int port = 51129; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "https", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = port, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DangerousAcceptAnyServerCertificateValidator = true + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"https://localhost:{port}", "/", 200, "Hello from Laura", port)) + .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_dangerous_accept_any_server_certificate_validator() + { + int port = 52129; + + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamScheme = "https", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = port, + } + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + DangerousAcceptAnyServerCertificateValidator = false + } + } + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"https://localhost:{port}", "/", 200, "Hello from Laura", port)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody, int port) + { + _builder = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel(options => + { + options.Listen(IPAddress.Loopback, port, listenOptions => + { + listenOptions.UseHttps("idsrv3test.pfx", "idsrv3test"); + }); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; + + if(_downstreamPath != basePath) + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync("downstream path didnt match base path"); + } + else + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + } + }); + }) + .Build(); + + _builder.Start(); + } + + internal void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPath) + { + _downstreamPath.ShouldBe(expectedDownstreamPath); + } + + public void Dispose() + { + _builder?.Dispose(); + _steps.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 0de7113d..aac5be0d 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -26,6 +26,7 @@ using System.IO.Compression; using System.Text; using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; using Ocelot.Requester; +using Ocelot.Middleware.Multiplexer; namespace Ocelot.AcceptanceTests { @@ -40,17 +41,54 @@ namespace Ocelot.AcceptanceTests public string RequestIdKey = "OcRequestId"; private readonly Random _random; private IWebHostBuilder _webHostBuilder; + private WebHostBuilder _ocelotBuilder; + private IWebHost _ocelotHost; public Steps() { _random = new Random(); } + public async Task StartFakeOcelotWithWebSockets() + { + _ocelotBuilder = new WebHostBuilder(); + _ocelotBuilder.ConfigureServices(s => + { + s.AddSingleton(_ocelotBuilder); + s.AddOcelot(); + }); + _ocelotBuilder.UseKestrel() + .UseUrls("http://localhost:5000") + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .Configure(app => + { + app.UseWebSockets(); + app.UseOcelot().Wait(); + }) + .UseIISIntegration(); + _ocelotHost = _ocelotBuilder.Build(); + await _ocelotHost.StartAsync(); + } + public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) { var configurationPath = TestConfiguration.ConfigurationPath; - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); if (File.Exists(configurationPath)) { @@ -62,7 +100,7 @@ namespace Ocelot.AcceptanceTests public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration, string configurationPath) { - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); if (File.Exists(configurationPath)) { @@ -86,7 +124,7 @@ namespace Ocelot.AcceptanceTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(s => @@ -114,7 +152,7 @@ namespace Ocelot.AcceptanceTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(s => @@ -141,12 +179,6 @@ namespace Ocelot.AcceptanceTests _ocelotClient = _ocelotServer.CreateClient(); } -/* - public void GivenIHaveAddedXForwardedForHeader(string value) - { - _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation("X-Forwarded-For", value); - }*/ - public void GivenOcelotIsRunningWithMiddleareBeforePipeline(Func callback) { _webHostBuilder = new WebHostBuilder(); @@ -158,7 +190,7 @@ namespace Ocelot.AcceptanceTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(s => @@ -189,7 +221,7 @@ namespace Ocelot.AcceptanceTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(s => @@ -209,6 +241,39 @@ namespace Ocelot.AcceptanceTests _ocelotClient = _ocelotServer.CreateClient(); } + public void GivenOcelotIsRunningWithSpecficAggregatorsRegisteredInDi() + where TAggregator : class, IDefinedAggregator + where TDepedency : class + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddJsonFile("ocelot.json"); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddSingleton(); + s.AddOcelot() + .AddSingletonDefinedAggregator(); + }) + .Configure(a => + { + a.UseOcelot().Wait(); + }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() where TOne : DelegatingHandler where TWo : DelegatingHandler @@ -222,7 +287,7 @@ namespace Ocelot.AcceptanceTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(s => @@ -254,7 +319,7 @@ namespace Ocelot.AcceptanceTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(s => @@ -274,6 +339,11 @@ namespace Ocelot.AcceptanceTests _ocelotClient = _ocelotServer.CreateClient(); } + internal void GivenIAddCookieToMyRequest(string cookie) + { + _ocelotClient.DefaultRequestHeaders.Add("Set-Cookie", cookie); + } + /// /// 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. /// @@ -288,7 +358,7 @@ namespace Ocelot.AcceptanceTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(s => @@ -313,6 +383,12 @@ namespace Ocelot.AcceptanceTests header.First().ShouldBe(value); } + public void ThenTheTraceHeaderIsSet(string key) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldNotBeNullOrEmpty(); + } + public void GivenOcelotIsRunningUsingJsonSerializedCache() { _webHostBuilder = new WebHostBuilder(); @@ -324,7 +400,7 @@ namespace Ocelot.AcceptanceTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(s => @@ -361,7 +437,7 @@ namespace Ocelot.AcceptanceTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(s => @@ -389,7 +465,7 @@ namespace Ocelot.AcceptanceTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(s => @@ -449,7 +525,7 @@ namespace Ocelot.AcceptanceTests var builder = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile("configuration.json") + .AddJsonFile("ocelot.json") .AddEnvironmentVariables(); var configuration = builder.Build(); @@ -687,6 +763,7 @@ namespace Ocelot.AcceptanceTests { _ocelotClient?.Dispose(); _ocelotServer?.Dispose(); + _ocelotHost?.Dispose(); } public void ThenTheRequestIdIsReturned() diff --git a/test/Ocelot.AcceptanceTests/Steps.cs.orig b/test/Ocelot.AcceptanceTests/Steps.cs.orig deleted file mode 100644 index 84c119ab..00000000 --- a/test/Ocelot.AcceptanceTests/Steps.cs.orig +++ /dev/null @@ -1,722 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; -using CacheManager.Core; -using IdentityServer4.AccessTokenValidation; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Ocelot.Configuration.File; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; -using Shouldly; -using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; -using Ocelot.AcceptanceTests.Caching; -<<<<<<< HEAD -using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; -||||||| merged common ancestors -======= -using System.IO.Compression; -using System.Text; ->>>>>>> develop - -namespace Ocelot.AcceptanceTests -{ - public class Steps : IDisposable - { - private TestServer _ocelotServer; - private HttpClient _ocelotClient; - private HttpResponseMessage _response; - private HttpContent _postContent; - private BearerToken _token; - public HttpClient OcelotClient => _ocelotClient; - public string RequestIdKey = "OcRequestId"; - private readonly Random _random; - private IWebHostBuilder _webHostBuilder; - - public Steps() - { - _random = new Random(); - } - - public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) - { - var configurationPath = TestConfiguration.ConfigurationPath; - - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - } - - public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration, string configurationPath) - { - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); - - if (File.Exists(configurationPath)) - { - File.Delete(configurationPath); - } - - File.WriteAllText(configurationPath, jsonConfiguration); - } - - /// - /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. - /// - public void GivenOcelotIsRunning() - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl) - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddOpenTracing(option => - { - //this is the url that the butterfly collector server is running on... - option.CollectorUrl = butterflyUrl; - option.Service = "Ocelot"; - }); - }) - .Configure(app => - { - app.Use(async (context, next) => - { - await next.Invoke(); - }); - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - -/* - public void GivenIHaveAddedXForwardedForHeader(string value) - { - _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation("X-Forwarded-For", value); - }*/ - - public void GivenOcelotIsRunningWithMiddleareBeforePipeline(Func callback) - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .Configure(app => - { - app.UseMiddleware(callback); - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningWithHandlersRegisteredInDi() - where TOne : DelegatingHandler - where TWo : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddOcelot() - .AddDelegatingHandler() - .AddDelegatingHandler(); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningWithHandlersRegisteredInDi(FakeDependency dependency) - where TOne : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddSingleton(dependency); - s.AddOcelot() - .AddDelegatingHandler(); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _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(Action options, string authenticationProviderKey) - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - s.AddAuthentication() - .AddIdentityServerAuthentication(authenticationProviderKey, options); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void ThenTheResponseHeaderIs(string key, string value) - { - var header = _response.Headers.GetValues(key); - header.First().ShouldBe(value); - } - - public void GivenOcelotIsRunningUsingJsonSerializedCache() - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddCacheManager((x) => - { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithJsonSerializer() - .WithHandle(typeof(InMemoryJsonHandle<>)); - }); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningUsingConsulToStoreConfig() - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot().AddStoreOcelotConfigurationInConsul(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache() - { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddCacheManager((x) => - { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithJsonSerializer() - .WithHandle(typeof(InMemoryJsonHandle<>)); - }) - .AddStoreOcelotConfigurationInConsul(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - internal void ThenTheResponseShouldBe(FileConfiguration expecteds) - { - var response = JsonConvert.DeserializeObject(_response.Content.ReadAsStringAsync().Result); - - response.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); - response.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); - response.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); - - for (var i = 0; i < response.ReRoutes.Count; i++) - { - for (var j = 0; j < response.ReRoutes[i].DownstreamHostAndPorts.Count; j++) - { - var result = response.ReRoutes[i].DownstreamHostAndPorts[j]; - var expected = expecteds.ReRoutes[i].DownstreamHostAndPorts[j]; - result.Host.ShouldBe(expected.Host); - result.Port.ShouldBe(expected.Port); - } - - response.ReRoutes[i].DownstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].DownstreamPathTemplate); - response.ReRoutes[i].DownstreamScheme.ShouldBe(expecteds.ReRoutes[i].DownstreamScheme); - response.ReRoutes[i].UpstreamPathTemplate.ShouldBe(expecteds.ReRoutes[i].UpstreamPathTemplate); - response.ReRoutes[i].UpstreamHttpMethod.ShouldBe(expecteds.ReRoutes[i].UpstreamHttpMethod); - } - } - - /// - /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. - /// - public void GivenOcelotIsRunning(OcelotPipelineConfiguration ocelotPipelineConfig) - { - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile("configuration.json") - .AddEnvironmentVariables(); - - var configuration = builder.Build(); - _webHostBuilder = new WebHostBuilder(); - _webHostBuilder.ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - }); - - _ocelotServer = new TestServer(_webHostBuilder - .UseConfiguration(configuration) - .ConfigureServices(s => - { - Action settings = (x) => - { - x.WithMicrosoftLogging(log => - { - log.AddConsole(LogLevel.Debug); - }) - .WithDictionaryHandle(); - }; - - s.AddOcelot(configuration); - }) - .ConfigureLogging(l => - { - l.AddConsole(); - l.AddDebug(); - }) - .Configure(a => - { - a.UseOcelot(ocelotPipelineConfig).Wait(); - })); - - _ocelotClient = _ocelotServer.CreateClient(); - } - - public void GivenIHaveAddedATokenToMyRequest() - { - _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } - - public void GivenIHaveAToken(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "client"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } - - public void GivenIHaveATokenForApiReadOnlyScope(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.readOnly"), - 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 GivenIHaveATokenForApi2(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "client"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "api2"), - new KeyValuePair("username", "test"), - new KeyValuePair("password", "test"), - new KeyValuePair("grant_type", "password") - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) - { - var response = httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } - - public void GivenIHaveAnOcelotToken(string adminPath) - { - var tokenUrl = $"{adminPath}/connect/token"; - var formData = new List> - { - new KeyValuePair("client_id", "admin"), - new KeyValuePair("client_secret", "secret"), - new KeyValuePair("scope", "admin"), - new KeyValuePair("username", "admin"), - new KeyValuePair("password", "admin"), - new KeyValuePair("grant_type", "password") - }; - var content = new FormUrlEncodedContent(formData); - - var response = _ocelotClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - - public void VerifyIdentiryServerStarted(string url) - { - using (var httpClient = new HttpClient()) - { - var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; - response.EnsureSuccessStatusCode(); - } - } - - public void WhenIGetUrlOnTheApiGateway(string url) - { - _response = _ocelotClient.GetAsync(url).Result; - } - - public void GivenIAddAHeader(string key, string value) - { - _ocelotClient.DefaultRequestHeaders.Add(key, value); - } - - public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) - { - var tasks = new Task[times]; - - for (int i = 0; i < times; i++) - { - var urlCopy = url; - tasks[i] = GetForServiceDiscoveryTest(urlCopy); - Thread.Sleep(_random.Next(40, 60)); - } - - Task.WaitAll(tasks); - } - - private async Task GetForServiceDiscoveryTest(string url) - { - var response = await _ocelotClient.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - int count = int.Parse(content); - count.ShouldBeGreaterThan(0); - } - - public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) - { - for (int i = 0; i < times; i++) - { - var clientId = "ocelotclient1"; - var request = new HttpRequestMessage(new HttpMethod("GET"), url); - request.Headers.Add("ClientId", clientId); - _response = _ocelotClient.SendAsync(request).Result; - } - } - - public void WhenIGetUrlOnTheApiGateway(string url, string requestId) - { - _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); - - _response = _ocelotClient.GetAsync(url).Result; - } - - public void WhenIPostUrlOnTheApiGateway(string url) - { - _response = _ocelotClient.PostAsync(url, _postContent).Result; - } - - public void GivenThePostHasContent(string postcontent) - { - _postContent = new StringContent(postcontent); - } - - public void GivenThePostHasGzipContent(object input) - { - string json = JsonConvert.SerializeObject(input); - byte[] jsonBytes = Encoding.UTF8.GetBytes(json); - MemoryStream ms = new MemoryStream(); - using (GZipStream gzip = new GZipStream(ms, CompressionMode.Compress, true)) - { - gzip.Write(jsonBytes, 0, jsonBytes.Length); - } - ms.Position = 0; - StreamContent content = new StreamContent(ms); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - content.Headers.ContentEncoding.Add("gzip"); - _postContent = content; - } - - public void ThenTheResponseBodyShouldBe(string expectedBody) - { - _response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody); - } - - public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) - { - _response.StatusCode.ShouldBe(expectedHttpStatusCode); - } - - public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode) - { - var responseStatusCode = (int)_response.StatusCode; - responseStatusCode.ShouldBe(expectedHttpStatusCode); - } - - public void Dispose() - { - _ocelotClient?.Dispose(); - _ocelotServer?.Dispose(); - } - - public void ThenTheRequestIdIsReturned() - { - _response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); - } - - public void ThenTheRequestIdIsReturned(string expected) - { - _response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); - } - - public void ThenTheContentLengthIs(int expected) - { - _response.Content.Headers.ContentLength.ShouldBe(expected); - } - - public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() - { - int numberOfRequests = 100; - var aggregateUrl = "/"; - var aggregateExpected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; - var tomUrl = "/tom"; - var tomExpected = "{Hello from Tom}"; - var lauraUrl = "/laura"; - var lauraExpected = "{Hello from Laura}"; - var random = new Random(); - - var aggregateTasks = new Task[numberOfRequests]; - - for (int i = 0; i < numberOfRequests; i++) - { - aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random); - } - - var tomTasks = new Task[numberOfRequests]; - - for (int i = 0; i < numberOfRequests; i++) - { - tomTasks[i] = Fire(tomUrl, tomExpected, random); - } - - var lauraTasks = new Task[numberOfRequests]; - - for (int i = 0; i < numberOfRequests; i++) - { - lauraTasks[i] = Fire(lauraUrl, lauraExpected, random); - } - - Task.WaitAll(lauraTasks); - Task.WaitAll(tomTasks); - Task.WaitAll(aggregateTasks); - } - - private async Task Fire(string url, string expectedBody, Random random) - { - var request = new HttpRequestMessage(new HttpMethod("GET"), url); - await Task.Delay(random.Next(0, 2)); - var response = await _ocelotClient.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); - content.ShouldBe(expectedBody); - } - } -} diff --git a/test/Ocelot.AcceptanceTests/TestConfiguration.cs b/test/Ocelot.AcceptanceTests/TestConfiguration.cs index 5f29d1bd..6f5818ca 100644 --- a/test/Ocelot.AcceptanceTests/TestConfiguration.cs +++ b/test/Ocelot.AcceptanceTests/TestConfiguration.cs @@ -5,6 +5,6 @@ namespace Ocelot.AcceptanceTests { public static class TestConfiguration { - public static string ConfigurationPath => Path.Combine(AppContext.BaseDirectory, "configuration.json"); + public static string ConfigurationPath => Path.Combine(AppContext.BaseDirectory, "ocelot.json"); } } diff --git a/test/Ocelot.AcceptanceTests/WebSocketTests.cs b/test/Ocelot.AcceptanceTests/WebSocketTests.cs new file mode 100644 index 00000000..dce2f306 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/WebSocketTests.cs @@ -0,0 +1,482 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Consul; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Ocelot.Configuration.File; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.AcceptanceTests +{ + public class WebSocketTests : IDisposable + { + private IWebHost _firstDownstreamHost; + private IWebHost _secondDownstreamHost; + private readonly List _secondRecieved; + private readonly List _firstRecieved; + private readonly List _serviceEntries; + private readonly Steps _steps; + private IWebHost _fakeConsulBuilder; + + public WebSocketTests() + { + _steps = new Steps(); + _firstRecieved = new List(); + _secondRecieved = new List(); + _serviceEntries = new List(); + } + + [Fact] + public void should_proxy_websocket_input_to_downstream_service() + { + var downstreamPort = 5001; + var downstreamHost = "localhost"; + + var config = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamPathTemplate = "/", + DownstreamPathTemplate = "/ws", + DownstreamScheme = "ws", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = downstreamHost, + Port = downstreamPort + } + } + } + } + }; + + this.Given(_ => _steps.GivenThereIsAConfiguration(config)) + .And(_ => _steps.StartFakeOcelotWithWebSockets()) + .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) + .When(_ => StartClient("ws://localhost:5000/")) + .Then(_ => _firstRecieved.Count.ShouldBe(10)) + .BDDfy(); + } + + [Fact] + public void should_proxy_websocket_input_to_downstream_service_and_use_load_balancer() + { + var downstreamPort = 5005; + var downstreamHost = "localhost"; + var secondDownstreamPort = 5006; + var secondDownstreamHost = "localhost"; + + var config = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamPathTemplate = "/", + DownstreamPathTemplate = "/ws", + DownstreamScheme = "ws", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = downstreamHost, + Port = downstreamPort + }, + new FileHostAndPort + { + Host = secondDownstreamHost, + Port = secondDownstreamPort + } + }, + LoadBalancer = "RoundRobin" + } + } + }; + + this.Given(_ => _steps.GivenThereIsAConfiguration(config)) + .And(_ => _steps.StartFakeOcelotWithWebSockets()) + .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) + .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}","/ws")) + .When(_ => WhenIStartTheClients()) + .Then(_ => ThenBothDownstreamServicesAreCalled()) + .BDDfy(); + } + + [Fact] + public void should_proxy_websocket_input_to_downstream_service_and_use_service_discovery_and_load_balancer() + { + var downstreamPort = 5007; + var downstreamHost = "localhost"; + + var secondDownstreamPort = 5008; + var secondDownstreamHost = "localhost"; + + var serviceName = "websockets"; + var consulPort = 8509; + var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = serviceName, + Address = downstreamHost, + Port = downstreamPort, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + var serviceEntryTwo = new ServiceEntry() + { + Service = new AgentService() + { + Service = serviceName, + Address = secondDownstreamHost, + Port = secondDownstreamPort, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + var config = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + UpstreamPathTemplate = "/", + DownstreamPathTemplate = "/ws", + DownstreamScheme = "ws", + LoadBalancer = "RoundRobin", + ServiceName = serviceName, + UseServiceDiscovery = true + } + }, + GlobalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "localhost", + Port = consulPort, + Type = "consul" + } + } + }; + + this.Given(_ => _steps.GivenThereIsAConfiguration(config)) + .And(_ => _steps.StartFakeOcelotWithWebSockets()) + .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(fakeConsulServiceDiscoveryUrl, serviceName)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) + .And(_ => StartFakeDownstreamService($"http://{downstreamHost}:{downstreamPort}", "/ws")) + .And(_ => StartSecondFakeDownstreamService($"http://{secondDownstreamHost}:{secondDownstreamPort}", "/ws")) + .When(_ => WhenIStartTheClients()) + .Then(_ => ThenBothDownstreamServicesAreCalled()) + .BDDfy(); + } + + private void ThenBothDownstreamServicesAreCalled() + { + _firstRecieved.Count.ShouldBe(10); + _firstRecieved.ForEach(x => + { + x.ShouldBe("test"); + }); + + _secondRecieved.Count.ShouldBe(10); + _secondRecieved.ForEach(x => + { + x.ShouldBe("chocolate"); + }); + } + + private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) + { + foreach (var serviceEntry in serviceEntries) + { + _serviceEntries.Add(serviceEntry); + } + } + + private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url, string serviceName) + { + _fakeConsulBuilder = new WebHostBuilder() + .UseUrls(url) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseUrls(url) + .Configure(app => + { + app.Run(async context => + { + if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") + { + await context.Response.WriteJsonAsync(_serviceEntries); + } + }); + }) + .Build(); + + _fakeConsulBuilder.Start(); + } + + private async Task WhenIStartTheClients() + { + var firstClient = StartClient("ws://localhost:5000/"); + + var secondClient = StartSecondClient("ws://localhost:5000/"); + + await Task.WhenAll(firstClient, secondClient); + } + + private async Task StartClient(string url) + { + var client = new ClientWebSocket(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + string line = "test"; + for (int i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartSecondClient(string url) + { + await Task.Delay(500); + + var client = new ClientWebSocket(); + + await client.ConnectAsync(new Uri(url), CancellationToken.None); + + var sending = Task.Run(async () => + { + string line = "test"; + for (int i = 0; i < 10; i++) + { + var bytes = Encoding.UTF8.GetBytes(line); + + await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, + CancellationToken.None); + await Task.Delay(10); + } + + await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + }); + + var receiving = Task.Run(async () => + { + var buffer = new byte[1024 * 4]; + + while (true) + { + var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + if (result.MessageType == WebSocketMessageType.Text) + { + _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); + } + else if (result.MessageType == WebSocketMessageType.Close) + { + await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); + break; + } + } + }); + + await Task.WhenAll(sending, receiving); + } + + private async Task StartFakeDownstreamService(string url, string path) + { + _firstDownstreamHost = new WebHostBuilder() + .ConfigureServices(s => { }).UseKestrel() + .UseUrls(url) + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .Configure(app => + { + app.UseWebSockets(); + app.Use(async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Echo(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + }) + .UseIISIntegration().Build(); + await _firstDownstreamHost.StartAsync(); + } + + private async Task StartSecondFakeDownstreamService(string url, string path) + { + _secondDownstreamHost = new WebHostBuilder() + .ConfigureServices(s => { }).UseKestrel() + .UseUrls(url) + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .Configure(app => + { + app.UseWebSockets(); + app.Use(async (context, next) => + { + if (context.Request.Path == path) + { + if (context.WebSockets.IsWebSocketRequest) + { + WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync(); + await Message(webSocket); + } + else + { + context.Response.StatusCode = 400; + } + } + else + { + await next(); + } + }); + }) + .UseIISIntegration().Build(); + await _secondDownstreamHost.StartAsync(); + } + + private async Task Echo(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private async Task Message(WebSocket webSocket) + { + try + { + var buffer = new byte[1024 * 4]; + + var bytes = Encoding.UTF8.GetBytes("chocolate"); + + var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + + while (!result.CloseStatus.HasValue) + { + await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, CancellationToken.None); + + result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + } + + await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + public void Dispose() + { + _steps.Dispose(); + _firstDownstreamHost?.Dispose(); + _secondDownstreamHost?.Dispose(); + _fakeConsulBuilder?.Dispose(); + } + } +} diff --git a/test/Ocelot.AcceptanceTests/appsettings.json b/test/Ocelot.AcceptanceTests/appsettings.json index 6d9b7c55..36499f4d 100644 --- a/test/Ocelot.AcceptanceTests/appsettings.json +++ b/test/Ocelot.AcceptanceTests/appsettings.json @@ -6,5 +6,19 @@ "System": "Error", "Microsoft": "Error" } + }, + "spring": { + "application": { + "name": "ocelot" + } + }, + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": true, + "shouldFetchRegistry": true, + "port": 5000, + "hostName": "localhost" + } } } diff --git a/test/Ocelot.AcceptanceTests/appsettings.product.json b/test/Ocelot.AcceptanceTests/appsettings.product.json new file mode 100644 index 00000000..8d8f06c7 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/appsettings.product.json @@ -0,0 +1,24 @@ +{ + "Logging": { + "IncludeScopes": true, + "LogLevel": { + "Default": "Error", + "System": "Error", + "Microsoft": "Error" + } + }, + "spring": { + "application": { + "name": "product" + } + }, + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": true + }, + "instance": { + "port": 50371 + } + } +} diff --git a/test/Ocelot.AcceptanceTests/configuration.json b/test/Ocelot.AcceptanceTests/configuration.json deleted file mode 100755 index 027025b0..00000000 --- a/test/Ocelot.AcceptanceTests/configuration.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "ReRoutes": [ - { - "DownstreamPathTemplate": "41879/", - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": "Get", - "AuthenticationOptions": { - "Provider": null, - "ProviderRootUrl": null, - "ApiName": null, - "RequireHttps": false, - "AllowedScopes": [], - "ApiSecret": null - }, - "AddHeadersToRequest": {}, - "AddClaimsToRequest": {}, - "RouteClaimsRequirement": {}, - "AddQueriesToRequest": {}, - "RequestIdKey": null, - "FileCacheOptions": { - "TtlSeconds": 0 - }, - "ReRouteIsCaseSensitive": false, - "ServiceName": null, - "DownstreamScheme": "http", - "DownstreamHost": "localhost", - "DownstreamPort": 41879, - "QoSOptions": { - "ExceptionsAllowedBeforeBreaking": 0, - "DurationOfBreak": 0, - "TimeoutValue": 0 - }, - "LoadBalancer": null, - "RateLimitOptions": { - "ClientWhitelist": [], - "EnableRateLimiting": false, - "Period": null, - "PeriodTimespan": 0, - "Limit": 0 - } - } - ], - "GlobalConfiguration": { - "RequestIdKey": null, - "ServiceDiscoveryProvider": { - "Provider": null, - "Host": null, - "Port": 0 - }, - "AdministrationPath": null, - "RateLimitOptions": { - "ClientIdHeader": "ClientId", - "QuotaExceededMessage": null, - "RateLimitCounterPrefix": "ocelot", - "DisableRateLimitHeaders": false, - "HttpStatusCode": 429 - } - } -} \ No newline at end of file diff --git a/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs b/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs new file mode 100644 index 00000000..3e9e1a4a --- /dev/null +++ b/test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.IO; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Middleware; +using Ocelot.DependencyInjection; +using System.Net.Http; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes.Jobs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Validators; + +namespace Ocelot.Benchmarks +{ + [Config(typeof(AllTheThingsBenchmarks))] + public class AllTheThingsBenchmarks : ManualConfig + { + private IWebHost _service; + private IWebHost _ocelot; + private HttpClient _httpClient; + + public AllTheThingsBenchmarks() + { + Add(StatisticColumn.AllStatistics); + Add(MemoryDiagnoser.Default); + Add(BaselineValidator.FailOnError); + } + + [GlobalSetup] + public void SetUp() + { + var configuration = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "localhost", + Port = 51879, + } + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + } + } + }; + + GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty); + GivenThereIsAConfiguration(configuration); + GivenOcelotIsRunning("http://localhost:5000"); + + _httpClient = new HttpClient(); + } + + [Benchmark(Baseline = true)] + public async Task Baseline() + { + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000/"); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + // * Summary * + // BenchmarkDotNet=v0.10.13, OS=macOS 10.12.6 (16G1212) [Darwin 16.7.0] + // Intel Core i5-4278U CPU 2.60GHz (Haswell), 1 CPU, 4 logical cores and 2 physical cores + // .NET Core SDK=2.1.4 + // [Host] : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT + // DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT + + // Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s | Scaled | Gen 0 | Gen 1 | Allocated | + // --------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|------:|-------:|--------:|-------:|----------:| + // Baseline | 2.102 ms | 0.0292 ms | 0.0273 ms | 0.0070 ms | 2.063 ms | 2.080 ms | 2.093 ms | 2.122 ms | 2.152 ms | 475.8 | 1.00 | 31.2500 | 3.9063 | 1.63 KB | + + private void GivenOcelotIsRunning(string url) + { + _ocelot = new WebHostBuilder() + .UseKestrel() + .UseUrls(url) + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json") + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => { + s.AddOcelot(); + }) + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + }) + .UseIISIntegration() + .Configure(app => + { + app.UseOcelot().Wait(); + }) + .Build(); + + _ocelot.Start(); + } + + public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = Path.Combine(AppContext.BaseDirectory, "ocelot.json");; + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _service = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _service.Start(); + } + } +} diff --git a/test/Ocelot.Benchmarks/ExceptionHandlerMiddlewareBenchmarks.cs b/test/Ocelot.Benchmarks/ExceptionHandlerMiddlewareBenchmarks.cs new file mode 100644 index 00000000..9b1f3312 --- /dev/null +++ b/test/Ocelot.Benchmarks/ExceptionHandlerMiddlewareBenchmarks.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.IO; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.Middleware; +using Ocelot.DependencyInjection; +using System.Net.Http; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes.Jobs; +using Ocelot.Configuration.Repository; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; +using Ocelot.Errors.Middleware; +using Microsoft.Extensions.DependencyInjection; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Validators; + +namespace Ocelot.Benchmarks +{ + [SimpleJob(launchCount: 1, warmupCount: 2, targetCount: 5)] + [Config(typeof(ExceptionHandlerMiddlewareBenchmarks))] + public class ExceptionHandlerMiddlewareBenchmarks : ManualConfig + { + private ExceptionHandlerMiddleware _middleware; + private DownstreamContext _downstreamContext; + private OcelotRequestDelegate _next; + + public ExceptionHandlerMiddlewareBenchmarks() + { + Add(StatisticColumn.AllStatistics); + Add(MemoryDiagnoser.Default); + Add(BaselineValidator.FailOnError); + } + + [GlobalSetup] + public void SetUp() + { + var serviceCollection = new ServiceCollection(); + var config = new ConfigurationRoot(new List()); + var builder = new OcelotBuilder(serviceCollection, config); + var services = serviceCollection.BuildServiceProvider(); + var loggerFactory = services.GetService(); + var configRepo = services.GetService(); + var repo = services.GetService(); + _next = async context => { + await Task.CompletedTask; + throw new Exception("BOOM"); + }; + _middleware = new ExceptionHandlerMiddleware(_next, loggerFactory, configRepo, repo); + _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + } + + [Benchmark(Baseline = true)] + public async Task Baseline() + { + await _middleware.Invoke(_downstreamContext); + } + } +} diff --git a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj index 8c63914c..7761dab1 100644 --- a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +++ b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj @@ -19,7 +19,7 @@ - + all diff --git a/test/Ocelot.Benchmarks/Program.cs b/test/Ocelot.Benchmarks/Program.cs index 56b87404..8601eeaa 100644 --- a/test/Ocelot.Benchmarks/Program.cs +++ b/test/Ocelot.Benchmarks/Program.cs @@ -7,7 +7,9 @@ namespace Ocelot.Benchmarks public static void Main(string[] args) { var switcher = new BenchmarkSwitcher(new[] { - typeof(UrlPathToUrlPathTemplateMatcherBenchmarks), + typeof(UrlPathToUrlPathTemplateMatcherBenchmarks), + typeof(AllTheThingsBenchmarks), + typeof(ExceptionHandlerMiddlewareBenchmarks) }); switcher.Run(args); diff --git a/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs b/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs index d7f4434b..3f02f48b 100644 --- a/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs +++ b/test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs @@ -1,6 +1,9 @@ +using System; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Columns; using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Validators; using Ocelot.DownstreamRouteFinder.UrlMatcher; namespace Ocelot.Benchmarks @@ -15,6 +18,8 @@ namespace Ocelot.Benchmarks public UrlPathToUrlPathTemplateMatcherBenchmarks() { Add(StatisticColumn.AllStatistics); + Add(MemoryDiagnoser.Default); + Add(BaselineValidator.FailOnError); } [GlobalSetup] @@ -25,16 +30,23 @@ namespace Ocelot.Benchmarks _downstreamUrlPathTemplate = "api/product/products/{productId}/variants/"; } - [Benchmark] - public void Benchmark1() + [Benchmark(Baseline = true)] + public void Baseline() { _urlPathMatcher.Match(_downstreamUrlPath, _downstreamUrlPathTemplate); } - [Benchmark] - public void Benchmark2() - { - _urlPathMatcher.Match(_downstreamUrlPath, _downstreamUrlPathTemplate); - } + // * Summary * + + // BenchmarkDotNet=v0.10.13, OS=macOS 10.12.6 (16G1212) [Darwin 16.7.0] + // Intel Core i5-4278U CPU 2.60GHz (Haswell), 1 CPU, 4 logical cores and 2 physical cores + // .NET Core SDK=2.1.4 + // [Host] : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT + // DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT + + + // Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s | + // ----------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|----------:| + // Benchmark1 | 3.133 us | 0.0492 us | 0.0460 us | 0.0119 us | 3.082 us | 3.100 us | 3.122 us | 3.168 us | 3.233 us | 319,161.9 | } } diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 763d642c..84b0c0bb 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -461,7 +461,7 @@ namespace Ocelot.IntegrationTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(x => @@ -579,7 +579,7 @@ namespace Ocelot.IntegrationTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(x => { @@ -612,7 +612,7 @@ namespace Ocelot.IntegrationTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(x => @@ -652,7 +652,7 @@ namespace Ocelot.IntegrationTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(x => { @@ -675,7 +675,7 @@ namespace Ocelot.IntegrationTests private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) { - var configurationPath = $"{Directory.GetCurrentDirectory()}/configuration.json"; + var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); @@ -688,7 +688,7 @@ namespace Ocelot.IntegrationTests var text = File.ReadAllText(configurationPath); - configurationPath = $"{AppContext.BaseDirectory}/configuration.json"; + configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; if (File.Exists(configurationPath)) { diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index 8dc250b2..97c8782f 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -14,7 +14,7 @@ ..\..\codeanalysis.ruleset - + PreserveNewest @@ -25,26 +25,26 @@ - - + + all - - - - - - - + + + + + + + - - + + - + - + \ No newline at end of file diff --git a/test/Ocelot.IntegrationTests/RaftTests.cs b/test/Ocelot.IntegrationTests/RaftTests.cs index b8c24d5a..baad5091 100644 --- a/test/Ocelot.IntegrationTests/RaftTests.cs +++ b/test/Ocelot.IntegrationTests/RaftTests.cs @@ -301,7 +301,7 @@ namespace Ocelot.IntegrationTests private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) { - var configurationPath = $"{Directory.GetCurrentDirectory()}/configuration.json"; + var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); @@ -314,7 +314,7 @@ namespace Ocelot.IntegrationTests var text = File.ReadAllText(configurationPath); - configurationPath = $"{AppContext.BaseDirectory}/configuration.json"; + configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; if (File.Exists(configurationPath)) { @@ -340,9 +340,11 @@ namespace Ocelot.IntegrationTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddJsonFile("peers.json", optional: true, reloadOnChange: true); + #pragma warning disable CS0618 config.AddOcelotBaseUrl(url); + #pragma warning restore CS0618 config.AddEnvironmentVariables(); }) .ConfigureServices(x => diff --git a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs index e61fa63b..ed125aa4 100644 --- a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs +++ b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs @@ -108,7 +108,7 @@ namespace Ocelot.IntegrationTests var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - config.AddJsonFile("configuration.json"); + config.AddJsonFile("ocelot.json"); config.AddEnvironmentVariables(); }) .ConfigureServices(x => @@ -138,7 +138,7 @@ namespace Ocelot.IntegrationTests private void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) { - var configurationPath = $"{Directory.GetCurrentDirectory()}/configuration.json"; + var configurationPath = $"{Directory.GetCurrentDirectory()}/ocelot.json"; var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); @@ -151,7 +151,7 @@ namespace Ocelot.IntegrationTests var text = File.ReadAllText(configurationPath); - configurationPath = $"{AppContext.BaseDirectory}/configuration.json"; + configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; if (File.Exists(configurationPath)) { diff --git a/test/Ocelot.IntegrationTests/configuration.json b/test/Ocelot.IntegrationTests/ocelot.json old mode 100755 new mode 100644 similarity index 100% rename from test/Ocelot.IntegrationTests/configuration.json rename to test/Ocelot.IntegrationTests/ocelot.json diff --git a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj index 1e708c68..a43e6b23 100644 --- a/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj +++ b/test/Ocelot.ManualTest/Ocelot.ManualTest.csproj @@ -16,24 +16,24 @@ - + PreserveNewest - + - - - - - - - - - - + + + + + + + + + + all diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 4e728d3d..dcc7676d 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -1,13 +1,13 @@ -using System.IO; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Configuration; -using Ocelot.DependencyInjection; -using Ocelot.Middleware; - -namespace Ocelot.ManualTest +namespace Ocelot.ManualTest { + using System.IO; + using Microsoft.AspNetCore.Hosting; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Ocelot.DependencyInjection; + using Ocelot.Middleware; + public class Program { public static void Main(string[] args) @@ -21,7 +21,7 @@ namespace Ocelot.ManualTest .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) .AddJsonFile("appsettings.json", true, true) .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) - .AddJsonFile("configuration.json") + .AddJsonFile("ocelot.json") .AddEnvironmentVariables(); }) .ConfigureServices(s => { @@ -32,17 +32,17 @@ namespace Ocelot.ManualTest x.Audience = "test"; }); - s.AddOcelot() - .AddCacheManager(x => - { - x.WithDictionaryHandle(); - }) - .AddOpenTracing(option => - { - option.CollectorUrl = "http://localhost:9618"; - option.Service = "Ocelot.ManualTest"; - }) - .AddAdministration("/administration", "secret"); + s.AddOcelot() + .AddCacheManager(x => + { + x.WithDictionaryHandle(); + }) + /*.AddOpenTracing(option => + { + option.CollectorUrl = "http://localhost:9618"; + option.Service = "Ocelot.ManualTest"; + })*/ + .AddAdministration("/administration", "secret"); }) .ConfigureLogging((hostingContext, logging) => { diff --git a/test/Ocelot.ManualTest/appsettings.json b/test/Ocelot.ManualTest/appsettings.json index e3439bab..16fe363b 100644 --- a/test/Ocelot.ManualTest/appsettings.json +++ b/test/Ocelot.ManualTest/appsettings.json @@ -2,9 +2,15 @@ "Logging": { "IncludeScopes": false, "LogLevel": { - "Default": "Debug", + "Default": "Error", "System": "Error", "Microsoft": "Error" } + }, + "eureka": { + "client": { + "serviceUrl": "http://localhost:8761/eureka/", + "shouldRegisterWithEureka": true + } } } diff --git a/test/Ocelot.ManualTest/configuration.json b/test/Ocelot.ManualTest/ocelot.json similarity index 95% rename from test/Ocelot.ManualTest/configuration.json rename to test/Ocelot.ManualTest/ocelot.json index ba34d4e5..c21f8abb 100644 --- a/test/Ocelot.ManualTest/configuration.json +++ b/test/Ocelot.ManualTest/ocelot.json @@ -1,5 +1,20 @@ { "ReRoutes": [ + { + "DownstreamPathTemplate": "/profile", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/profile", + "UpstreamHttpMethod": [ "Get" ], + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 3000 + } + ], + "QoSOptions": { + "TimeoutValue": 360000 + } + }, { "DownstreamPathTemplate": "/api/values", "DownstreamScheme": "http", @@ -99,7 +114,7 @@ "HttpHandlerOptions": { "AllowAutoRedirect": true, "UseCookieContainer": true, - "UseTracing": false + "UseTracing": true }, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareRealCacheTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareRealCacheTests.cs index 210b1cb5..b3bbface 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareRealCacheTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareRealCacheTests.cs @@ -1,7 +1,4 @@ -using Ocelot.Errors; -using Ocelot.Middleware; - -namespace Ocelot.UnitTests.Cache +namespace Ocelot.UnitTests.Cache { using System.Linq; using System.Net; @@ -17,15 +14,17 @@ namespace Ocelot.UnitTests.Cache using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; + using Ocelot.Middleware; using TestStack.BDDfy; using Xunit; using Microsoft.AspNetCore.Http; + using Ocelot.Middleware.Multiplexer; public class OutputCacheMiddlewareRealCacheTests { - private IOcelotCache _cacheManager; - private OutputCacheMiddleware _middleware; - private DownstreamContext _downstreamContext; + private readonly IOcelotCache _cacheManager; + private readonly OutputCacheMiddleware _middleware; + private readonly DownstreamContext _downstreamContext; private OcelotRequestDelegate _next; private Mock _loggerFactory; private IRegionCreator _regionCreator; @@ -43,7 +42,7 @@ namespace Ocelot.UnitTests.Cache }); _cacheManager = new OcelotCacheManagerCache(cacheManagerOutputCache); _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _downstreamContext.DownstreamRequest = new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"); + _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); _next = context => Task.CompletedTask; _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager, _regionCreator); } @@ -56,14 +55,10 @@ namespace Ocelot.UnitTests.Cache Headers = { ContentType = new MediaTypeHeaderValue("application/json")} }; - var response = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = content, - }; + var response = new DownstreamResponse(content, HttpStatusCode.OK, new List>>()); this.Given(x => x.GivenResponseIsNotCached(response)) .And(x => x.GivenTheDownstreamRouteIs()) - .And(x => x.GivenThereAreNoErrors()) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheContentTypeHeaderIsCached()) .BDDfy(); @@ -81,9 +76,9 @@ namespace Ocelot.UnitTests.Cache header.First().ShouldBe("application/json"); } - private void GivenResponseIsNotCached(HttpResponseMessage message) + private void GivenResponseIsNotCached(DownstreamResponse response) { - _downstreamContext.DownstreamResponse = message; + _downstreamContext.DownstreamResponse = response; } private void GivenTheDownstreamRouteIs() @@ -96,10 +91,5 @@ namespace Ocelot.UnitTests.Cache _downstreamContext.DownstreamReRoute = reRoute; } - - private void GivenThereAreNoErrors() - { - _downstreamContext.Errors = new List(); - } } } diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index 78911787..cd0f9719 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -1,8 +1,4 @@ -using System.Net; -using Ocelot.Errors; -using Ocelot.Middleware; - -namespace Ocelot.UnitTests.Cache +namespace Ocelot.UnitTests.Cache { using System; using System.Collections.Generic; @@ -19,17 +15,20 @@ namespace Ocelot.UnitTests.Cache using Ocelot.Logging; using TestStack.BDDfy; using Xunit; + using System.Net; + using Ocelot.Middleware; + using Ocelot.Middleware.Multiplexer; public class OutputCacheMiddlewareTests { private readonly Mock> _cacheManager; - private Mock _loggerFactory; + private readonly Mock _loggerFactory; private Mock _logger; private OutputCacheMiddleware _middleware; - private DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; + private readonly DownstreamContext _downstreamContext; + private readonly OcelotRequestDelegate _next; private CachedResponse _response; - private IRegionCreator _regionCreator; + private readonly IRegionCreator _regionCreator; public OutputCacheMiddlewareTests() { @@ -40,13 +39,23 @@ namespace Ocelot.UnitTests.Cache _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; - _downstreamContext.DownstreamRequest = new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"); + _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); } [Fact] public void should_returned_cached_item_when_it_is_in_cache() { - var cachedResponse = new CachedResponse(HttpStatusCode.OK, new Dictionary>(), "", new Dictionary>()); + var headers = new Dictionary> + { + { "test", new List { "test" } } + }; + + var contentHeaders = new Dictionary> + { + { "content-type", new List { "application/json" } } + }; + + var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, "", contentHeaders); this.Given(x => x.GivenThereIsACachedResponse(cachedResponse)) .And(x => x.GivenTheDownstreamRouteIs()) .When(x => x.WhenICallTheMiddleware()) @@ -59,7 +68,6 @@ namespace Ocelot.UnitTests.Cache { this.Given(x => x.GivenResponseIsNotCached()) .And(x => x.GivenTheDownstreamRouteIs()) - .And(x => x.GivenThereAreNoErrors()) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenTheCacheAddIsCalledCorrectly()) .BDDfy(); @@ -81,7 +89,7 @@ namespace Ocelot.UnitTests.Cache private void GivenResponseIsNotCached() { - _downstreamContext.DownstreamResponse = new HttpResponseMessage(); + _downstreamContext.DownstreamResponse = new DownstreamResponse(new HttpResponseMessage()); } private void GivenTheDownstreamRouteIs() @@ -101,11 +109,6 @@ namespace Ocelot.UnitTests.Cache _downstreamContext.DownstreamReRoute = downstreamRoute.ReRoute.DownstreamReRoute[0]; } - private void GivenThereAreNoErrors() - { - _downstreamContext.Errors = new List(); - } - private void ThenTheCacheGetIsCalledCorrectly() { _cacheManager diff --git a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs index 522d853f..cda6d5de 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs @@ -72,7 +72,7 @@ namespace Ocelot.UnitTests.Configuration private void ThenTheLoggerIsCalledCorrectly() { _logger - .Verify(x => x.LogDebug(It.IsAny(), It.IsAny()), Times.Once); + .Verify(x => x.LogDebug(It.IsAny()), Times.Once); } private void ThenClaimsToThingsAreReturned() diff --git a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs index 4a6faba2..7393b631 100644 --- a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationPollerTests.cs @@ -1,16 +1,17 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Threading; using Moq; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.Logging; using Ocelot.Responses; +using Ocelot.UnitTests.Responder; using TestStack.BDDfy; using Xunit; using Shouldly; -using static Ocelot.UnitTests.Wait; +using static Ocelot.Infrastructure.Wait; namespace Ocelot.UnitTests.Configuration { @@ -21,6 +22,7 @@ namespace Ocelot.UnitTests.Configuration private Mock _repo; private Mock _setter; private FileConfiguration _fileConfig; + private Mock _config; public ConsulFileConfigurationPollerTests() { @@ -30,10 +32,12 @@ namespace Ocelot.UnitTests.Configuration _repo = new Mock(); _setter = new Mock(); _fileConfig = new FileConfiguration(); + _config = new Mock(); _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(_fileConfig)); - _poller = new ConsulFileConfigurationPoller(_factory.Object, _repo.Object, _setter.Object); + _config.Setup(x => x.Delay).Returns(100); + _poller = new ConsulFileConfigurationPoller(_factory.Object, _repo.Object, _setter.Object, _config.Object); } - + public void Dispose() { _poller.Dispose(); @@ -42,7 +46,7 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void should_start() { - this.Given(x => ThenTheSetterIsCalled(_fileConfig)) + this.Given(x => ThenTheSetterIsCalled(_fileConfig, 1)) .BDDfy(); } @@ -65,22 +69,82 @@ namespace Ocelot.UnitTests.Configuration } }; - this.Given(x => WhenTheConfigIsChangedInConsul(newConfig)) - .Then(x => ThenTheSetterIsCalled(newConfig)) + this.Given(x => WhenTheConfigIsChangedInConsul(newConfig, 0)) + .Then(x => ThenTheSetterIsCalled(newConfig, 1)) .BDDfy(); } - private void WhenTheConfigIsChangedInConsul(FileConfiguration newConfig) + [Fact] + public void should_not_poll_if_already_polling() { - _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(newConfig)); + var newConfig = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "test" + } + }, + } + } + }; + + this.Given(x => WhenTheConfigIsChangedInConsul(newConfig, 10)) + .Then(x => ThenTheSetterIsCalled(newConfig, 1)) + .BDDfy(); } - private void ThenTheSetterIsCalled(FileConfiguration fileConfig) + [Fact] + public void should_do_nothing_if_call_to_consul_fails() + { + var newConfig = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "test" + } + }, + } + } + }; + + this.Given(x => WhenConsulErrors()) + .Then(x => ThenTheSetterIsCalled(newConfig, 0)) + .BDDfy(); + } + + private void WhenConsulErrors() + { + _repo + .Setup(x => x.Get()) + .ReturnsAsync(new ErrorResponse(new AnyError())); + } + + private void WhenTheConfigIsChangedInConsul(FileConfiguration newConfig, int delay) + { + _repo + .Setup(x => x.Get()) + .Callback(() => Thread.Sleep(delay)) + .ReturnsAsync(new OkResponse(newConfig)); + } + + private void ThenTheSetterIsCalled(FileConfiguration fileConfig, int times) { var result = WaitFor(2000).Until(() => { try { - _setter.Verify(x => x.Set(fileConfig), Times.Once); + _setter.Verify(x => x.Set(fileConfig), Times.Exactly(times)); return true; } catch(Exception) diff --git a/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationRepositoryTests.cs new file mode 100644 index 00000000..b6b1cc6d --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/ConsulFileConfigurationRepositoryTests.cs @@ -0,0 +1,137 @@ +namespace Ocelot.UnitTests.Configuration +{ + using Xunit; + using TestStack.BDDfy; + using Shouldly; + using Ocelot.Configuration.Repository; + using Moq; + using Ocelot.Infrastructure.Consul; + using Ocelot.Logging; + using Ocelot.Configuration.File; + using Ocelot.Cache; + using System; + using System.Collections.Generic; + using Ocelot.Responses; + using System.Threading.Tasks; + using Ocelot.Configuration; + using Ocelot.Configuration.Builder; + using Ocelot.ServiceDiscovery.Configuration; + using Consul; + using Newtonsoft.Json; + using System.Text; + using System.Threading; + using System.Linq; + + public class ConsulFileConfigurationRepositoryTests + { + private ConsulFileConfigurationRepository _repo; + private Mock> _cache; + private Mock _internalRepo; + private Mock _factory; + private Mock _loggerFactory; + private Mock _client; + private Mock _kvEndpoint; + private FileConfiguration _fileConfiguration; + private Response _result; + + public ConsulFileConfigurationRepositoryTests() + { + _cache = new Mock>(); + _internalRepo = new Mock(); + _loggerFactory = new Mock(); + + _factory = new Mock(); + _client = new Mock(); + _kvEndpoint = new Mock(); + + _client + .Setup(x => x.KV) + .Returns(_kvEndpoint.Object); + _factory + .Setup(x => x.Get(It.IsAny())) + .Returns(_client.Object); + + _internalRepo + .Setup(x => x.Get()) + .Returns(new OkResponse(new InternalConfiguration(new List(), "", new ServiceProviderConfigurationBuilder().Build(), ""))); + + _repo = new ConsulFileConfigurationRepository(_cache.Object, _internalRepo.Object, _factory.Object, _loggerFactory.Object); + } + + [Fact] + public void should_set_config() + { + var config = FakeFileConfiguration(); + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenWritingToConsulSucceeds()) + .When(_ => WhenISetTheConfiguration()) + .Then(_ => ThenTheConfigurationIsStoredAs(config)) + .BDDfy(); + } + + private void GivenWritingToConsulSucceeds() + { + var response = new WriteResult(); + response.Response = true; + + _kvEndpoint + .Setup(x => x.Put(It.IsAny(), It.IsAny())).ReturnsAsync(response); + } + + private void ThenTheConfigurationIsStoredAs(FileConfiguration config) + { + var json = JsonConvert.SerializeObject(config, Formatting.Indented); + + var bytes = Encoding.UTF8.GetBytes(json); + + _kvEndpoint + .Verify(x => x.Put(It.Is(k => k.Value.SequenceEqual(bytes)), It.IsAny()), Times.Once); + } + + private async Task WhenISetTheConfiguration() + { + _result = await _repo.Set(_fileConfiguration); + } + + private void GivenIHaveAConfiguration(FileConfiguration config) + { + _fileConfiguration = config; + } + + private FileConfiguration FakeFileConfiguration() + { + var reRoutes = new List + { + new FileReRoute + { + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "123.12.12.12", + Port = 80, + } + }, + DownstreamScheme = "https", + DownstreamPathTemplate = "/asdfs/test/{test}" + } + }; + + var globalConfiguration = new FileGlobalConfiguration + { + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Port = 198, + Host = "blah" + } + }; + + return new FileConfiguration + { + GlobalConfiguration = globalConfiguration, + ReRoutes = reRoutes + }; + } + } +} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs similarity index 68% rename from test/Ocelot.UnitTests/Configuration/FileConfigurationRepositoryTests.cs rename to test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs index 15dd3729..c2e77b38 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DiskFileConfigurationRepositoryTests.cs @@ -1,29 +1,34 @@ -using System; -using System.Collections.Generic; -using Moq; -using Ocelot.Configuration.File; -using Shouldly; -using TestStack.BDDfy; -using Xunit; -using Newtonsoft.Json; -using System.IO; -using Microsoft.AspNetCore.Hosting; -using Ocelot.Configuration.Repository; - namespace Ocelot.UnitTests.Configuration { - public class FileConfigurationRepositoryTests + using System; + using System.Collections.Generic; + using Moq; + using Ocelot.Configuration.File; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + using Newtonsoft.Json; + using System.IO; + using Microsoft.AspNetCore.Hosting; + using Ocelot.Configuration.Repository; + + public class DiskFileConfigurationRepositoryTests { private readonly Mock _hostingEnvironment = new Mock(); private IFileConfigurationRepository _repo; + private string _configurationPath; private FileConfiguration _result; private FileConfiguration _fileConfiguration; - private string _environmentName = "DEV"; - public FileConfigurationRepositoryTests() + // This is a bit dirty and it is dev.dev so that the ConfigurationBuilderExtensionsTests + // cant pick it up if they run in parralel..sigh these are not really unit + // tests but whatever... + private string _environmentName = "DEV.DEV"; + + public DiskFileConfigurationRepositoryTests() { _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); - _repo = new FileConfigurationRepository(_hostingEnvironment.Object); + _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object); } [Fact] @@ -31,9 +36,9 @@ namespace Ocelot.UnitTests.Configuration { var config = FakeFileConfigurationForGet(); - this.Given(x => x.GivenTheConfigurationIs(config)) - .When(x => x.WhenIGetTheReRoutes()) - .Then(x => x.ThenTheFollowingIsReturned(config)) + this.Given(_ => GivenTheConfigurationIs(config)) + .When(_ => WhenIGetTheReRoutes()) + .Then(_ => ThenTheFollowingIsReturned(config)) .BDDfy(); } @@ -42,10 +47,10 @@ namespace Ocelot.UnitTests.Configuration { var config = FakeFileConfigurationForGet(); - this.Given(x => x.GivenTheEnvironmentNameIsUnavailable()) - .And(x => x.GivenTheConfigurationIs(config)) - .When(x => x.WhenIGetTheReRoutes()) - .Then(x => x.ThenTheFollowingIsReturned(config)) + this.Given(_ => GivenTheEnvironmentNameIsUnavailable()) + .And(_ => GivenTheConfigurationIs(config)) + .When(_ => WhenIGetTheReRoutes()) + .Then(_ => ThenTheFollowingIsReturned(config)) .BDDfy(); } @@ -54,9 +59,10 @@ namespace Ocelot.UnitTests.Configuration { var config = FakeFileConfigurationForSet(); - this.Given(x => GivenIHaveAConfiguration(config)) - .When(x => WhenISetTheConfiguration()) - .Then(x => ThenTheConfigurationIsStoredAs(config)) + this.Given(_ => GivenIHaveAConfiguration(config)) + .When(_ => WhenISetTheConfiguration()) + .Then(_ => ThenTheConfigurationIsStoredAs(config)) + .And(_ => ThenTheConfigurationJsonIsIndented(config)) .BDDfy(); } @@ -64,10 +70,12 @@ namespace Ocelot.UnitTests.Configuration public void should_set_file_configuration_if_environment_name_is_unavailable() { var config = FakeFileConfigurationForSet(); - this.Given(x => GivenIHaveAConfiguration(config)) - .And(x => GivenTheEnvironmentNameIsUnavailable()) - .When(x => WhenISetTheConfiguration()) - .Then(x => ThenTheConfigurationIsStoredAs(config)) + + this.Given(_ => GivenIHaveAConfiguration(config)) + .And(_ => GivenTheEnvironmentNameIsUnavailable()) + .When(_ => WhenISetTheConfiguration()) + .Then(_ => ThenTheConfigurationIsStoredAs(config)) + .And(_ => ThenTheConfigurationJsonIsIndented(config)) .BDDfy(); } @@ -75,7 +83,7 @@ namespace Ocelot.UnitTests.Configuration { _environmentName = null; _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(_environmentName); - _repo = new FileConfigurationRepository(_hostingEnvironment.Object); + _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object); } private void GivenIHaveAConfiguration(FileConfiguration fileConfiguration) @@ -113,16 +121,25 @@ namespace Ocelot.UnitTests.Configuration private void GivenTheConfigurationIs(FileConfiguration fileConfiguration) { - var configurationPath = $"{AppContext.BaseDirectory}/configuration{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; + _configurationPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); - if (File.Exists(configurationPath)) + if (File.Exists(_configurationPath)) { - File.Delete(configurationPath); + File.Delete(_configurationPath); } - File.WriteAllText(configurationPath, jsonConfiguration); + File.WriteAllText(_configurationPath, jsonConfiguration); + } + + private void ThenTheConfigurationJsonIsIndented(FileConfiguration expecteds) + { + var path = !string.IsNullOrEmpty(_configurationPath) ? _configurationPath : _configurationPath = $"{AppContext.BaseDirectory}/ocelot{(string.IsNullOrEmpty(_environmentName) ? string.Empty : ".")}{_environmentName}.json"; + + var resultText = File.ReadAllText(_configurationPath); + var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); + resultText.ShouldBe(expectedText); } private void WhenIGetTheReRoutes() diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs index 79aff9ed..40fe4f02 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationCreatorTests.cs @@ -1,22 +1,18 @@ -using System.Collections.Generic; -using Castle.Components.DictionaryAdapter; -using Microsoft.Extensions.Options; -using Moq; -using Ocelot.Cache; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.File; -using Ocelot.Configuration.Validator; -using Ocelot.Logging; -using Ocelot.Responses; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration +namespace Ocelot.UnitTests.Configuration { - using System; + using System.Collections.Generic; + using Moq; + using Ocelot.Cache; + using Ocelot.Configuration; + using Ocelot.Configuration.Builder; + using Ocelot.Configuration.Creator; + using Ocelot.Configuration.File; + using Ocelot.Configuration.Validator; + using Ocelot.Logging; + using Ocelot.Responses; + using Shouldly; + using TestStack.BDDfy; + using Xunit; using Ocelot.DependencyInjection; using Ocelot.Errors; using Ocelot.UnitTests.TestData; @@ -24,23 +20,22 @@ namespace Ocelot.UnitTests.Configuration public class FileConfigurationCreatorTests { - private readonly Mock> _fileConfig; private readonly Mock _validator; - private Response _config; + private Response _config; private FileConfiguration _fileConfiguration; private readonly Mock _logger; - private readonly FileOcelotConfigurationCreator _ocelotConfigurationCreator; - private Mock _claimsToThingCreator; - private Mock _authOptionsCreator; - private Mock _upstreamTemplatePatternCreator; - private Mock _requestIdKeyCreator; - private Mock _serviceProviderConfigCreator; - private Mock _qosOptionsCreator; - private Mock _fileReRouteOptionsCreator; - private Mock _rateLimitOptions; - private Mock _regionCreator; - private Mock _httpHandlerOptionsCreator; - private Mock _adminPath; + private readonly FileInternalConfigurationCreator _internalConfigurationCreator; + private readonly Mock _claimsToThingCreator; + private readonly Mock _authOptionsCreator; + private readonly Mock _upstreamTemplatePatternCreator; + private readonly Mock _requestIdKeyCreator; + private readonly Mock _serviceProviderConfigCreator; + private readonly Mock _qosOptionsCreator; + private readonly Mock _fileReRouteOptionsCreator; + private readonly Mock _rateLimitOptions; + private readonly Mock _regionCreator; + private readonly Mock _httpHandlerOptionsCreator; + private readonly Mock _adminPath; private readonly Mock _headerFindAndReplaceCreator; private readonly Mock _downstreamAddressesCreator; @@ -48,7 +43,6 @@ namespace Ocelot.UnitTests.Configuration { _logger = new Mock(); _validator = new Mock(); - _fileConfig = new Mock>(); _claimsToThingCreator = new Mock(); _authOptionsCreator = new Mock(); _upstreamTemplatePatternCreator = new Mock(); @@ -63,8 +57,7 @@ namespace Ocelot.UnitTests.Configuration _headerFindAndReplaceCreator = new Mock(); _downstreamAddressesCreator = new Mock(); - _ocelotConfigurationCreator = new FileOcelotConfigurationCreator( - _fileConfig.Object, + _internalConfigurationCreator = new FileInternalConfigurationCreator( _validator.Object, _logger.Object, _claimsToThingCreator.Object, @@ -134,7 +127,8 @@ namespace Ocelot.UnitTests.Configuration { "Tom", "Laura" - } + }, + Aggregator = "asdf" } } }; @@ -261,7 +255,7 @@ namespace Ocelot.UnitTests.Configuration .And(x => x.GivenTheConfigIsValid()) .And(x => x.GivenTheFollowingRegionIsReturned("region")) .When(x => x.WhenICreateTheConfig()) - .Then(x => x.ThenTheRegionCreatorIsCalledCorrectly("region")) + .Then(x => x.ThenTheRegionCreatorIsCalledCorrectly()) .And(x => x.ThenTheHeaderFindAndReplaceCreatorIsCalledCorrectly()) .BDDfy(); } @@ -799,14 +793,11 @@ namespace Ocelot.UnitTests.Configuration private void GivenTheConfigIs(FileConfiguration fileConfiguration) { _fileConfiguration = fileConfiguration; - _fileConfig - .Setup(x => x.Value) - .Returns(_fileConfiguration); } private void WhenICreateTheConfig() { - _config = _ocelotConfigurationCreator.Create(_fileConfiguration).Result; + _config = _internalConfigurationCreator.Create(_fileConfiguration).Result; } private void ThenTheReRoutesAre(List expectedReRoutes) @@ -826,7 +817,9 @@ namespace Ocelot.UnitTests.Configuration result.DownstreamReRoute[0].ClaimsToHeaders.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToHeaders.Count); result.DownstreamReRoute[0].ClaimsToQueries.Count.ShouldBe(expected.DownstreamReRoute[0].ClaimsToQueries.Count); result.DownstreamReRoute[0].RequestIdKey.ShouldBe(expected.DownstreamReRoute[0].RequestIdKey); - result.DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DownstreamReRoute[0].DelegatingHandlers); + result.DownstreamReRoute[0].DelegatingHandlers.ShouldBe(expected.DownstreamReRoute[0].DelegatingHandlers); + result.DownstreamReRoute[0].AddHeadersToDownstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToDownstream); + result.DownstreamReRoute[0].AddHeadersToUpstream.ShouldBe(expected.DownstreamReRoute[0].AddHeadersToUpstream, "AddHeadersToUpstream should be set"); } } @@ -909,7 +902,7 @@ namespace Ocelot.UnitTests.Configuration private void GivenTheHeaderFindAndReplaceCreatorReturns() { - _headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny())).Returns(new HeaderTransformations(new List(), new List())); + _headerFindAndReplaceCreator.Setup(x => x.Create(It.IsAny())).Returns(new HeaderTransformations(new List(), new List(), new List(), new List())); } private void GivenTheFollowingIsReturned(ServiceProviderConfiguration serviceProviderConfiguration) @@ -925,7 +918,7 @@ namespace Ocelot.UnitTests.Configuration .Returns(region); } - private void ThenTheRegionCreatorIsCalledCorrectly(string expected) + private void ThenTheRegionCreatorIsCalledCorrectly() { _regionCreator .Verify(x => x.Create(_fileConfiguration.ReRoutes[0]), Times.Once); diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs deleted file mode 100644 index 506da50c..00000000 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationProviderTests.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Collections.Generic; -using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.File; -using Ocelot.Responses; -using Shouldly; -using TestStack.BDDfy; -using Xunit; -using Newtonsoft.Json; -using System.IO; -using Ocelot.Configuration.Provider; -using Ocelot.Configuration.Repository; - -namespace Ocelot.UnitTests.Configuration -{ - public class FileConfigurationProviderTests - { - private readonly IFileConfigurationProvider _provider; - private Mock _repo; - private FileConfiguration _result; - private FileConfiguration _fileConfiguration; - - public FileConfigurationProviderTests() - { - _repo = new Mock(); - _provider = new FileConfigurationProvider(_repo.Object); - } - - [Fact] - public void should_return_file_configuration() - { - var config = new FileConfiguration(); - - this.Given(x => x.GivenTheConfigurationIs(config)) - .When(x => x.WhenIGetTheReRoutes()) - .Then(x => x.ThenTheRepoIsCalledCorrectly()) - .BDDfy(); - } - - private void GivenTheConfigurationIs(FileConfiguration fileConfiguration) - { - _fileConfiguration = fileConfiguration; - _repo - .Setup(x => x.Get()) - .ReturnsAsync(new OkResponse(fileConfiguration)); - } - - private void WhenIGetTheReRoutes() - { - _result = _provider.Get().Result.Data; - } - - private void ThenTheRepoIsCalledCorrectly() - { - _repo - .Verify(x => x.Get(), Times.Once); - } - } -} diff --git a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs index e16148f2..ff33872c 100644 --- a/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs +++ b/test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs @@ -18,19 +18,19 @@ namespace Ocelot.UnitTests.Configuration public class FileConfigurationSetterTests { private FileConfiguration _fileConfiguration; - private FileConfigurationSetter _configSetter; - private Mock _configRepo; - private Mock _configCreator; - private Response _configuration; + private FileAndInternalConfigurationSetter _configSetter; + private Mock _configRepo; + private Mock _configCreator; + private Response _configuration; private object _result; private Mock _repo; public FileConfigurationSetterTests() { _repo = new Mock(); - _configRepo = new Mock(); - _configCreator = new Mock(); - _configSetter = new FileConfigurationSetter(_configRepo.Object, _configCreator.Object, _repo.Object); + _configRepo = new Mock(); + _configCreator = new Mock(); + _configSetter = new FileAndInternalConfigurationSetter(_configRepo.Object, _configCreator.Object, _repo.Object); } [Fact] @@ -38,11 +38,11 @@ namespace Ocelot.UnitTests.Configuration { var fileConfig = new FileConfiguration(); var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - var config = new OcelotConfiguration(new List(), string.Empty, serviceProviderConfig, "asdf"); + var config = new InternalConfiguration(new List(), string.Empty, serviceProviderConfig, "asdf"); this.Given(x => GivenTheFollowingConfiguration(fileConfig)) .And(x => GivenTheRepoReturns(new OkResponse())) - .And(x => GivenTheCreatorReturns(new OkResponse(config))) + .And(x => GivenTheCreatorReturns(new OkResponse(config))) .When(x => WhenISetTheConfiguration()) .Then(x => ThenTheConfigurationRepositoryIsCalledCorrectly()) .BDDfy(); @@ -67,7 +67,7 @@ namespace Ocelot.UnitTests.Configuration this.Given(x => GivenTheFollowingConfiguration(fileConfig)) .And(x => GivenTheRepoReturns(new OkResponse())) - .And(x => GivenTheCreatorReturns(new ErrorResponse(It.IsAny()))) + .And(x => GivenTheCreatorReturns(new ErrorResponse(It.IsAny()))) .When(x => WhenISetTheConfiguration()) .And(x => ThenAnErrorResponseIsReturned()) .BDDfy(); @@ -85,7 +85,7 @@ namespace Ocelot.UnitTests.Configuration _result.ShouldBeOfType(); } - private void GivenTheCreatorReturns(Response configuration) + private void GivenTheCreatorReturns(Response configuration) { _configuration = configuration; _configCreator diff --git a/test/Ocelot.UnitTests/Configuration/HashMatcherTests.cs b/test/Ocelot.UnitTests/Configuration/HashMatcherTests.cs deleted file mode 100644 index 55c5edc9..00000000 --- a/test/Ocelot.UnitTests/Configuration/HashMatcherTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Ocelot.Configuration.Authentication; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - public class HashMatcherTests - { - private string _password; - private string _hash; - private string _salt; - private bool _result; - private HashMatcher _hashMatcher; - - public HashMatcherTests() - { - _hashMatcher = new HashMatcher(); - } - - [Fact] - public void should_match_hash() - { - var hash = "kE/mxd1hO9h9Sl2VhGhwJUd9xZEv4NP6qXoN39nIqM4="; - var salt = "zzWITpnDximUNKYLiUam/w=="; - var password = "secret"; - - this.Given(x => GivenThePassword(password)) - .And(x => GivenTheHash(hash)) - .And(x => GivenTheSalt(salt)) - .When(x => WhenIMatch()) - .Then(x => ThenTheResultIs(true)) - .BDDfy(); - } - - [Fact] - public void should_not_match_hash() - { - var hash = "kE/mxd1hO9h9Sl2VhGhwJUd9xZEv4NP6qXoN39nIqM4="; - var salt = "zzWITpnDximUNKYLiUam/w=="; - var password = "secret1"; - - this.Given(x => GivenThePassword(password)) - .And(x => GivenTheHash(hash)) - .And(x => GivenTheSalt(salt)) - .When(x => WhenIMatch()) - .Then(x => ThenTheResultIs(false)) - .BDDfy(); - } - - private void GivenThePassword(string password) - { - _password = password; - } - - private void GivenTheHash(string hash) - { - _hash = hash; - } - - private void GivenTheSalt(string salt) - { - _salt = salt; - } - - private void WhenIMatch() - { - _result = _hashMatcher.Match(_password, _salt, _hash); - } - - private void ThenTheResultIs(bool expected) - { - _result.ShouldBe(expected); - } - } -} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs index 37ee8957..a8f8644a 100644 --- a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -5,7 +5,11 @@ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; +using Ocelot.Infrastructure; +using Ocelot.Logging; using Ocelot.Middleware; +using Ocelot.Responses; +using Ocelot.UnitTests.Responder; using Shouldly; using TestStack.BDDfy; using Xunit; @@ -17,12 +21,17 @@ namespace Ocelot.UnitTests.Configuration private HeaderFindAndReplaceCreator _creator; private FileReRoute _reRoute; private HeaderTransformations _result; - private Mock _finder; + private Mock _placeholders; + private Mock _factory; + private Mock _logger; public HeaderFindAndReplaceCreatorTests() { - _finder = new Mock(); - _creator = new HeaderFindAndReplaceCreator(_finder.Object); + _logger = new Mock(); + _factory = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _placeholders = new Mock(); + _creator = new HeaderFindAndReplaceCreator(_placeholders.Object, _factory.Object); } [Fact] @@ -84,6 +93,40 @@ namespace Ocelot.UnitTests.Configuration .BDDfy(); } + [Fact] + public void should_log_errors_and_not_add_headers() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + }, + UpstreamHeaderTransform = new Dictionary + { + {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, + } + }; + + var expected = new List + { + }; + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => GivenTheBaseUrlErrors()) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingDownstreamIsReturned(expected)) + .And(x => ThenTheFollowingUpstreamIsReturned(expected)) + .And(x => ThenTheLoggerIsCalledCorrectly("Unable to add DownstreamHeaderTransform Location: http://www.bbc.co.uk/, {BaseUrl}")) + .And(x => ThenTheLoggerIsCalledCorrectly("Unable to add UpstreamHeaderTransform Location: http://www.bbc.co.uk/, {BaseUrl}")) + .BDDfy(); + } + + private void ThenTheLoggerIsCalledCorrectly(string message) + { + _logger.Verify(x => x.LogWarning(message), Times.Once); + } + [Fact] public void should_use_base_url_partial_placeholder() { @@ -107,9 +150,84 @@ namespace Ocelot.UnitTests.Configuration .BDDfy(); } + [Fact] + public void should_add_trace_id_header() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"Trace-Id", "{TraceId}"}, + } + }; + + var expected = new AddHeader("Trace-Id", "{TraceId}"); + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => GivenTheBaseUrlIs("http://ocelot.com/")) + .When(x => WhenICreate()) + .Then(x => ThenTheFollowingAddHeaderToDownstreamIsReturned(expected)) + .BDDfy(); + } + + [Fact] + public void should_add_downstream_header_as_is_when_no_replacement_is_given() + { + var reRoute = new FileReRoute + { + DownstreamHeaderTransform = new Dictionary + { + {"X-Custom-Header", "Value"}, + } + }; + + var expected = new AddHeader("X-Custom-Header", "Value"); + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => WhenICreate()) + .Then(x => x.ThenTheFollowingAddHeaderToDownstreamIsReturned(expected)) + .BDDfy(); + } + + [Fact] + public void should_add_upstream_header_as_is_when_no_replacement_is_given() + { + var reRoute = new FileReRoute + { + UpstreamHeaderTransform = new Dictionary + { + {"X-Custom-Header", "Value"}, + } + }; + + var expected = new AddHeader("X-Custom-Header", "Value"); + + this.Given(x => GivenTheReRoute(reRoute)) + .And(x => WhenICreate()) + .Then(x => x.ThenTheFollowingAddHeaderToUpstreamIsReturned(expected)) + .BDDfy(); + } + private void GivenTheBaseUrlIs(string baseUrl) { - _finder.Setup(x => x.Find()).Returns(baseUrl); + _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(baseUrl)); + } + + private void GivenTheBaseUrlErrors() + { + _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); + } + + private void ThenTheFollowingAddHeaderToDownstreamIsReturned(AddHeader addHeader) + { + _result.AddHeadersToDownstream[0].Key.ShouldBe(addHeader.Key); + _result.AddHeadersToDownstream[0].Value.ShouldBe(addHeader.Value); + } + + private void ThenTheFollowingAddHeaderToUpstreamIsReturned(AddHeader addHeader) + { + _result.AddHeadersToUpstream[0].Key.ShouldBe(addHeader.Key); + _result.AddHeadersToUpstream[0].Value.ShouldBe(addHeader.Value); } private void ThenTheFollowingDownstreamIsReturned(List downstream) diff --git a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs index b0b11bfc..7316dcb3 100644 --- a/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs @@ -1,6 +1,10 @@ -using Ocelot.Configuration; +using System; +using Butterfly.Client.Tracing; +using Butterfly.OpenTracing; +using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; +using Ocelot.Requester; using Shouldly; using TestStack.BDDfy; using Xunit; @@ -9,20 +13,61 @@ namespace Ocelot.UnitTests.Configuration { public class HttpHandlerOptionsCreatorTests { - private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; + private IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private FileReRoute _fileReRoute; private HttpHandlerOptions _httpHandlerOptions; + private IServiceTracer _serviceTracer; public HttpHandlerOptionsCreatorTests() { - _httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(); + _serviceTracer = new FakeServiceTracer(); + _httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(_serviceTracer); } [Fact] - public void should_create_options_with_useCookie_and_allowAutoRedirect_true_as_default() + public void should_not_use_tracing_if_fake_tracer_registered() + { + var fileReRoute = new FileReRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true + } + }; + + var expectedOptions = new HttpHandlerOptions(false, false, false); + + this.Given(x => GivenTheFollowing(fileReRoute)) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_use_tracing_if_real_tracer_registered() + { + var fileReRoute = new FileReRoute + { + HttpHandlerOptions = new FileHttpHandlerOptions + { + UseTracing = true + } + }; + + var expectedOptions = new HttpHandlerOptions(false, false, true); + + this.Given(x => GivenTheFollowing(fileReRoute)) + .And(x => GivenARealTracer()) + .When(x => WhenICreateHttpHandlerOptions()) + .Then(x => ThenTheFollowingOptionsReturned(expectedOptions)) + .BDDfy(); + } + + [Fact] + public void should_create_options_with_useCookie_false_and_allowAutoRedirect_true_as_default() { var fileReRoute = new FileReRoute(); - var expectedOptions = new HttpHandlerOptions(true, true, false); + var expectedOptions = new HttpHandlerOptions(false, false, false); this.Given(x => GivenTheFollowing(fileReRoute)) .When(x => WhenICreateHttpHandlerOptions()) @@ -61,12 +106,34 @@ namespace Ocelot.UnitTests.Configuration _httpHandlerOptions = _httpHandlerOptionsCreator.Create(_fileReRoute); } - private void ThenTheFollowingOptionsReturned(HttpHandlerOptions options) + private void ThenTheFollowingOptionsReturned(HttpHandlerOptions expected) { _httpHandlerOptions.ShouldNotBeNull(); - _httpHandlerOptions.AllowAutoRedirect.ShouldBe(options.AllowAutoRedirect); - _httpHandlerOptions.UseCookieContainer.ShouldBe(options.UseCookieContainer); - _httpHandlerOptions.UseTracing.ShouldBe(options.UseTracing); + _httpHandlerOptions.AllowAutoRedirect.ShouldBe(expected.AllowAutoRedirect); + _httpHandlerOptions.UseCookieContainer.ShouldBe(expected.UseCookieContainer); + _httpHandlerOptions.UseTracing.ShouldBe(expected.UseTracing); + } + + private void GivenARealTracer() + { + var tracer = new RealTracer(); + _httpHandlerOptionsCreator = new HttpHandlerOptionsCreator(tracer); + } + + class RealTracer : IServiceTracer + { + public ITracer Tracer => throw new NotImplementedException(); + + public string ServiceName => throw new NotImplementedException(); + + public string Environment => throw new NotImplementedException(); + + public string Identity => throw new NotImplementedException(); + + public ISpan Start(ISpanBuilder spanBuilder) + { + throw new NotImplementedException(); + } } } } diff --git a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs index cdc3f94d..50bf1f85 100644 --- a/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Configuration/InMemoryConfigurationRepositoryTests.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Repository; @@ -14,14 +12,14 @@ namespace Ocelot.UnitTests.Configuration { public class InMemoryConfigurationRepositoryTests { - private readonly InMemoryOcelotConfigurationRepository _repo; - private IOcelotConfiguration _config; + private readonly InMemoryInternalConfigurationRepository _repo; + private IInternalConfiguration _config; private Response _result; - private Response _getResult; + private Response _getResult; public InMemoryConfigurationRepositoryTests() { - _repo = new InMemoryOcelotConfigurationRepository(); + _repo = new InMemoryInternalConfigurationRepository(); } [Fact] @@ -49,7 +47,7 @@ namespace Ocelot.UnitTests.Configuration private void WhenIGetTheConfiguration() { - _getResult = _repo.Get().Result; + _getResult = _repo.Get(); } private void GivenThereIsASavedConfiguration() @@ -58,14 +56,14 @@ namespace Ocelot.UnitTests.Configuration WhenIAddOrReplaceTheConfig(); } - private void GivenTheConfigurationIs(IOcelotConfiguration config) + private void GivenTheConfigurationIs(IInternalConfiguration config) { _config = config; } private void WhenIAddOrReplaceTheConfig() { - _result = _repo.AddOrReplace(_config).Result; + _result = _repo.AddOrReplace(_config); } private void ThenNoErrorsAreReturned() @@ -73,7 +71,7 @@ namespace Ocelot.UnitTests.Configuration _result.IsError.ShouldBeFalse(); } - class FakeConfig : IOcelotConfiguration + class FakeConfig : IInternalConfiguration { private readonly string _downstreamTemplatePath; diff --git a/test/Ocelot.UnitTests/Configuration/OcelotConfigurationProviderTests.cs b/test/Ocelot.UnitTests/Configuration/OcelotConfigurationProviderTests.cs deleted file mode 100644 index 982fe091..00000000 --- a/test/Ocelot.UnitTests/Configuration/OcelotConfigurationProviderTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Collections.Generic; -using Moq; -using Ocelot.Configuration; -using Ocelot.Configuration.Builder; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.Provider; -using Ocelot.Configuration.Repository; -using Ocelot.Errors; -using Ocelot.Responses; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.Configuration -{ - public class OcelotConfigurationProviderTests - { - private readonly IOcelotConfigurationProvider _ocelotConfigurationProvider; - private readonly Mock _configurationRepository; - private Response _result; - - public OcelotConfigurationProviderTests() - { - _configurationRepository = new Mock(); - _ocelotConfigurationProvider = new OcelotConfigurationProvider(_configurationRepository.Object); - } - - [Fact] - public void should_get_config() - { - var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); - - this.Given(x => x.GivenTheRepoReturns(new OkResponse(new OcelotConfiguration(new List(), string.Empty, serviceProviderConfig, "")))) - .When(x => x.WhenIGetTheConfig()) - .Then(x => x.TheFollowingIsReturned(new OkResponse(new OcelotConfiguration(new List(), string.Empty, serviceProviderConfig, "")))) - .BDDfy(); - } - - [Fact] - public void should_return_error() - { - this.Given(x => x.GivenTheRepoReturns(new ErrorResponse(new List - { - new AnyError() - }))) - .When(x => x.WhenIGetTheConfig()) - .Then(x => x.TheFollowingIsReturned( - new ErrorResponse(new List - { - new AnyError() - }))) - .BDDfy(); - } - - private void GivenTheRepoReturns(Response config) - { - _configurationRepository - .Setup(x => x.Get()) - .ReturnsAsync(config); - } - - private void WhenIGetTheConfig() - { - _result = _ocelotConfigurationProvider.Get().Result; - } - - 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/ServiceProviderCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs index 8b0cc078..214d6d54 100644 --- a/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ServiceProviderCreatorTests.cs @@ -10,8 +10,7 @@ namespace Ocelot.UnitTests.Configuration { public class ServiceProviderCreatorTests { - private ServiceProviderConfigurationCreator _creator; - private FileReRoute _reRoute; + private readonly ServiceProviderConfigurationCreator _creator; private FileGlobalConfiguration _globalConfig; private ServiceProviderConfiguration _result; @@ -23,36 +22,30 @@ namespace Ocelot.UnitTests.Configuration [Fact] public void should_create_service_provider_config() { - var reRoute = new FileReRoute(); - var globalConfig = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Host = "127.0.0.1", Port = 1234, - Type = "ServiceFabric" + Type = "ServiceFabric", + Token = "testtoken" } }; var expected = new ServiceProviderConfigurationBuilder() - .WithServiceDiscoveryProviderHost("127.0.0.1") - .WithServiceDiscoveryProviderPort(1234) - .WithServiceDiscoveryProviderType("ServiceFabric") + .WithHost("127.0.0.1") + .WithPort(1234) + .WithType("ServiceFabric") + .WithToken("testtoken") .Build(); - this.Given(x => x.GivenTheFollowingReRoute(reRoute)) - .And(x => x.GivenTheFollowingGlobalConfig(globalConfig)) + this.Given(x => x.GivenTheFollowingGlobalConfig(globalConfig)) .When(x => x.WhenICreate()) .Then(x => x.ThenTheConfigIs(expected)) .BDDfy(); } - private void GivenTheFollowingReRoute(FileReRoute fileReRoute) - { - _reRoute = fileReRoute; - } - private void GivenTheFollowingGlobalConfig(FileGlobalConfiguration fileGlobalConfig) { _globalConfig = fileGlobalConfig; @@ -67,6 +60,8 @@ namespace Ocelot.UnitTests.Configuration { _result.Host.ShouldBe(expected.Host); _result.Port.ShouldBe(expected.Port); + _result.Token.ShouldBe(expected.Token); + _result.Type.ShouldBe(expected.Type); } } } diff --git a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs index 9dce0e50..c70bd440 100644 --- a/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs @@ -19,6 +19,38 @@ namespace Ocelot.UnitTests.Configuration _creator = new UpstreamTemplatePatternCreator(); } + [Fact] + public void should_use_re_route_priority() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/orders/{catchAll}", + Priority = 0 + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^(?i)/orders/[0-9a-zA-Z].*$")) + .And(x => ThenThePriorityIs(0)) + .BDDfy(); + } + + [Fact] + public void should_use_zero_priority() + { + var fileReRoute = new FileReRoute + { + UpstreamPathTemplate = "/{catchAll}", + Priority = 1 + }; + + this.Given(x => x.GivenTheFollowingFileReRoute(fileReRoute)) + .When(x => x.WhenICreateTheTemplatePattern()) + .Then(x => x.ThenTheFollowingIsReturned("^/.*")) + .And(x => ThenThePriorityIs(0)) + .BDDfy(); + } + [Fact] public void should_set_upstream_template_pattern_to_ignore_case_sensitivity() { diff --git a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs index c0ae8b87..520d8274 100644 --- a/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs +++ b/test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs @@ -8,32 +8,30 @@ using Ocelot.Responses; using TestStack.BDDfy; using Xunit; using Shouldly; -using Ocelot.Configuration.Provider; -using Microsoft.Extensions.DependencyInjection; using Ocelot.Raft; using Rafty.Concensus; -using Newtonsoft.Json; -using Rafty.FiniteStateMachine; using Ocelot.Configuration; namespace Ocelot.UnitTests.Controllers { + using Ocelot.Configuration.Repository; + public class FileConfigurationControllerTests { - private FileConfigurationController _controller; - private Mock _configGetter; - private Mock _configSetter; + private readonly FileConfigurationController _controller; + private readonly Mock _repo; + private readonly Mock _setter; private IActionResult _result; private FileConfiguration _fileConfiguration; - private Mock _provider; + private readonly Mock _provider; private Mock _node; public FileConfigurationControllerTests() { _provider = new Mock(); - _configGetter = new Mock(); - _configSetter = new Mock(); - _controller = new FileConfigurationController(_configGetter.Object, _configSetter.Object, _provider.Object); + _repo = new Mock(); + _setter = new Mock(); + _controller = new FileConfigurationController(_repo.Object, _setter.Object, _provider.Object); } [Fact] @@ -140,14 +138,14 @@ namespace Ocelot.UnitTests.Controllers private void GivenTheConfigSetterReturns(Response response) { - _configSetter + _setter .Setup(x => x.Set(It.IsAny())) .ReturnsAsync(response); } private void ThenTheConfigrationSetterIsCalledCorrectly() { - _configSetter + _setter .Verify(x => x.Set(_fileConfiguration), Times.Once); } @@ -168,7 +166,7 @@ namespace Ocelot.UnitTests.Controllers private void GivenTheGetConfigurationReturns(Ocelot.Responses.Response fileConfiguration) { - _configGetter + _repo .Setup(x => x.Get()) .ReturnsAsync(fileConfiguration); } @@ -180,7 +178,7 @@ namespace Ocelot.UnitTests.Controllers private void TheTheGetFileConfigurationIsCalledCorrectly() { - _configGetter + _repo .Verify(x => x.Get(), Times.Once); } diff --git a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs index 0fcbf66e..7c29977d 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs @@ -1,30 +1,219 @@ -using Microsoft.Extensions.Configuration; -using Ocelot.DependencyInjection; -using Shouldly; -using TestStack.BDDfy; -using Xunit; - -namespace Ocelot.UnitTests.DependencyInjection +namespace Ocelot.UnitTests.DependencyInjection { + using System.Collections.Generic; + using System.IO; + using Newtonsoft.Json; + using Ocelot.Configuration.File; + using Microsoft.Extensions.Configuration; + using Ocelot.DependencyInjection; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + public class ConfigurationBuilderExtensionsTests { private IConfigurationRoot _configuration; private string _result; + private IConfigurationRoot _configRoot; + private FileConfiguration _globalConfig; + private FileConfiguration _reRouteA; + private FileConfiguration _reRouteB; + private FileConfiguration _aggregate; [Fact] public void should_add_base_url_to_config() { - this.Given(x => GivenTheBaseUrl("test")) - .When(x => WhenIGet("BaseUrl")) - .Then(x => ThenTheResultIs("test")) + this.Given(_ => GivenTheBaseUrl("test")) + .When(_ => WhenIGet("BaseUrl")) + .Then(_ => ThenTheResultIs("test")) .BDDfy(); } + [Fact] + public void should_merge_files() + { + this.Given(_ => GivenMultipleConfigurationFiles()) + .When(_ => WhenIAddOcelotConfiguration()) + .Then(_ => ThenTheConfigsAreMerged()) + .BDDfy(); + } + + private void GivenMultipleConfigurationFiles() + { + _globalConfig = new FileConfiguration + { + GlobalConfiguration = new FileGlobalConfiguration + { + BaseUrl = "BaseUrl", + RateLimitOptions = new FileRateLimitOptions + { + HttpStatusCode = 500, + ClientIdHeader = "ClientIdHeader", + DisableRateLimitHeaders = true, + QuotaExceededMessage = "QuotaExceededMessage", + RateLimitCounterPrefix = "RateLimitCounterPrefix" + }, + ServiceDiscoveryProvider = new FileServiceDiscoveryProvider + { + Host = "Host", + Port = 80, + Type = "Type" + }, + RequestIdKey = "RequestIdKey" + } + }; + + _reRouteA = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamScheme = "DownstreamScheme", + DownstreamPathTemplate = "DownstreamPathTemplate", + Key = "Key", + UpstreamHost = "UpstreamHost", + UpstreamHttpMethod = new List + { + "UpstreamHttpMethod" + }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "Host", + Port = 80 + } + } + } + } + }; + + _reRouteB = new FileConfiguration + { + ReRoutes = new List + { + new FileReRoute + { + DownstreamScheme = "DownstreamSchemeB", + DownstreamPathTemplate = "DownstreamPathTemplateB", + Key = "KeyB", + UpstreamHost = "UpstreamHostB", + UpstreamHttpMethod = new List + { + "UpstreamHttpMethodB" + }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "HostB", + Port = 80 + } + } + }, + new FileReRoute + { + DownstreamScheme = "DownstreamSchemeBB", + DownstreamPathTemplate = "DownstreamPathTemplateBB", + Key = "KeyBB", + UpstreamHost = "UpstreamHostBB", + UpstreamHttpMethod = new List + { + "UpstreamHttpMethodBB" + }, + DownstreamHostAndPorts = new List + { + new FileHostAndPort + { + Host = "HostBB", + Port = 80 + } + } + } + } + }; + + _aggregate = new FileConfiguration + { + Aggregates = new List + { + new FileAggregateReRoute + { + ReRouteKeys = new List + { + "KeyB", + "KeyBB" + }, + UpstreamPathTemplate = "UpstreamPathTemplate", + }, + new FileAggregateReRoute + { + ReRouteKeys = new List + { + "KeyB", + "KeyBB" + }, + UpstreamPathTemplate = "UpstreamPathTemplate", + } + } + }; + + File.WriteAllText("ocelot.global.json", JsonConvert.SerializeObject(_globalConfig)); + File.WriteAllText("ocelot.reRoutesA.json", JsonConvert.SerializeObject(_reRouteA)); + File.WriteAllText("ocelot.reRoutesB.json", JsonConvert.SerializeObject(_reRouteB)); + File.WriteAllText("ocelot.aggregates.json", JsonConvert.SerializeObject(_aggregate)); + } + + private void WhenIAddOcelotConfiguration() + { + IConfigurationBuilder builder = new ConfigurationBuilder(); + builder.AddOcelot(); + _configRoot = builder.Build(); + } + + private void ThenTheConfigsAreMerged() + { + var fc = (FileConfiguration)_configRoot.Get(typeof(FileConfiguration)); + + fc.GlobalConfiguration.BaseUrl.ShouldBe(_globalConfig.GlobalConfiguration.BaseUrl); + fc.GlobalConfiguration.RateLimitOptions.ClientIdHeader.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.ClientIdHeader); + fc.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders); + fc.GlobalConfiguration.RateLimitOptions.HttpStatusCode.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.HttpStatusCode); + fc.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage); + fc.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix.ShouldBe(_globalConfig.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix); + fc.GlobalConfiguration.RequestIdKey.ShouldBe(_globalConfig.GlobalConfiguration.RequestIdKey); + fc.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(_globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Host); + fc.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(_globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Port); + fc.GlobalConfiguration.ServiceDiscoveryProvider.Type.ShouldBe(_globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Type); + + fc.ReRoutes.Count.ShouldBe(_reRouteA.ReRoutes.Count + _reRouteB.ReRoutes.Count); + + fc.ReRoutes.ShouldContain(x => x.DownstreamPathTemplate == _reRouteA.ReRoutes[0].DownstreamPathTemplate); + fc.ReRoutes.ShouldContain(x => x.DownstreamPathTemplate == _reRouteB.ReRoutes[0].DownstreamPathTemplate); + fc.ReRoutes.ShouldContain(x => x.DownstreamPathTemplate == _reRouteB.ReRoutes[1].DownstreamPathTemplate); + + fc.ReRoutes.ShouldContain(x => x.DownstreamScheme == _reRouteA.ReRoutes[0].DownstreamScheme); + fc.ReRoutes.ShouldContain(x => x.DownstreamScheme == _reRouteB.ReRoutes[0].DownstreamScheme); + fc.ReRoutes.ShouldContain(x => x.DownstreamScheme == _reRouteB.ReRoutes[1].DownstreamScheme); + + fc.ReRoutes.ShouldContain(x => x.Key == _reRouteA.ReRoutes[0].Key); + fc.ReRoutes.ShouldContain(x => x.Key == _reRouteB.ReRoutes[0].Key); + fc.ReRoutes.ShouldContain(x => x.Key == _reRouteB.ReRoutes[1].Key); + + fc.ReRoutes.ShouldContain(x => x.UpstreamHost == _reRouteA.ReRoutes[0].UpstreamHost); + fc.ReRoutes.ShouldContain(x => x.UpstreamHost == _reRouteB.ReRoutes[0].UpstreamHost); + fc.ReRoutes.ShouldContain(x => x.UpstreamHost == _reRouteB.ReRoutes[1].UpstreamHost); + + fc.Aggregates.Count.ShouldBe(_aggregate.Aggregates.Count); + } + private void GivenTheBaseUrl(string baseUrl) { + #pragma warning disable CS0618 var builder = new ConfigurationBuilder() .AddOcelotBaseUrl(baseUrl); - + #pragma warning restore CS0618 _configuration = builder.Build(); } diff --git a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs index df812f70..b295a564 100644 --- a/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs +++ b/test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs @@ -18,6 +18,8 @@ using Shouldly; using IdentityServer4.AccessTokenValidation; using TestStack.BDDfy; using Xunit; +using static Ocelot.UnitTests.Middleware.UserDefinedResponseAggregatorTests; +using Ocelot.Middleware.Multiplexer; namespace Ocelot.UnitTests.DependencyInjection { @@ -177,6 +179,40 @@ namespace Ocelot.UnitTests.DependencyInjection .BDDfy(); } + [Fact] + public void should_add_singleton_defined_aggregators() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => AddSingletonDefinedAggregator()) + .When(x => AddSingletonDefinedAggregator()) + .Then(x => ThenTheProviderIsRegisteredAndReturnsSpecificAggregators()) + .And(x => ThenTheAggregatorsAreSingleton()) + .BDDfy(); + } + + [Fact] + public void should_add_transient_defined_aggregators() + { + this.Given(x => WhenISetUpOcelotServices()) + .When(x => AddTransientDefinedAggregator()) + .When(x => AddTransientDefinedAggregator()) + .Then(x => ThenTheProviderIsRegisteredAndReturnsSpecificAggregators()) + .And(x => ThenTheAggregatorsAreTransient()) + .BDDfy(); + } + + private void AddSingletonDefinedAggregator() + where T : class, IDefinedAggregator + { + _ocelotBuilder.AddSingletonDefinedAggregator(); + } + + private void AddTransientDefinedAggregator() + where T : class, IDefinedAggregator + { + _ocelotBuilder.AddTransientDefinedAggregator(); + } + private void ThenTheSpecificHandlersAreSingleton() { var handlers = _serviceProvider.GetServices().ToList(); @@ -223,12 +259,14 @@ namespace Ocelot.UnitTests.DependencyInjection _ocelotBuilder.AddAdministration("/administration", options); } - private void AddTransientGlobalDelegatingHandler() where T : DelegatingHandler + private void AddTransientGlobalDelegatingHandler() + where T : DelegatingHandler { _ocelotBuilder.AddTransientDelegatingHandler(true); } - private void AddSpecificTransientDelegatingHandler() where T : DelegatingHandler + private void AddSpecificTransientDelegatingHandler() + where T : DelegatingHandler { _ocelotBuilder.AddTransientDelegatingHandler(); } @@ -256,13 +294,39 @@ namespace Ocelot.UnitTests.DependencyInjection handlers[1].ShouldBeOfType(); } + private void ThenTheProviderIsRegisteredAndReturnsSpecificAggregators() + { + _serviceProvider = _services.BuildServiceProvider(); + var handlers = _serviceProvider.GetServices().ToList(); + handlers[0].ShouldBeOfType(); + handlers[1].ShouldBeOfType(); + } + + private void ThenTheAggregatorsAreTransient() + { + var aggregators = _serviceProvider.GetServices().ToList(); + var first = aggregators[0]; + aggregators = _serviceProvider.GetServices().ToList(); + var second = aggregators[0]; + first.ShouldNotBe(second); + } + + private void ThenTheAggregatorsAreSingleton() + { + var aggregators = _serviceProvider.GetServices().ToList(); + var first = aggregators[0]; + aggregators = _serviceProvider.GetServices().ToList(); + var second = aggregators[0]; + first.ShouldBe(second); + } + private void OnlyOneVersionOfEachCacheIsRegistered() { var outputCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache)); var outputCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager)); var instance = (ICacheManager)outputCacheManager.ImplementationInstance; - var ocelotConfigCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache)); - var ocelotConfigCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager)); + var ocelotConfigCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache)); + var ocelotConfigCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager)); var fileConfigCache = _services.Single(x => x.ServiceType == typeof(IOcelotCache)); var fileConfigCacheManager = _services.Single(x => x.ServiceType == typeof(ICacheManager)); @@ -298,12 +362,14 @@ namespace Ocelot.UnitTests.DependencyInjection } } - private void AddGlobalDelegatingHandler() where T : DelegatingHandler + private void AddGlobalDelegatingHandler() + where T : DelegatingHandler { _ocelotBuilder.AddSingletonDelegatingHandler(true); } - private void AddSpecificDelegatingHandler() where T : DelegatingHandler + private void AddSpecificDelegatingHandler() + where T : DelegatingHandler { _ocelotBuilder.AddSingletonDelegatingHandler(); } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs index d3363b00..95bcaf47 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs @@ -1,7 +1,4 @@ -using Ocelot.Middleware; -using Ocelot.Middleware.Multiplexer; - -namespace Ocelot.UnitTests.DownstreamRouteFinder +namespace Ocelot.UnitTests.DownstreamRouteFinder { using System.Collections.Generic; using System.Threading.Tasks; @@ -9,7 +6,6 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; - using Ocelot.Configuration.Provider; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.Middleware; @@ -19,23 +15,26 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder using Shouldly; using TestStack.BDDfy; using Xunit; + using Ocelot.Configuration.Repository; + using Ocelot.Middleware; + using Ocelot.Middleware.Multiplexer; public class DownstreamRouteFinderMiddlewareTests { private readonly Mock _finder; - private readonly Mock _provider; + private readonly Mock _repo; private Response _downstreamRoute; - private IOcelotConfiguration _config; + private IInternalConfiguration _config; private Mock _loggerFactory; private Mock _logger; - private DownstreamRouteFinderMiddleware _middleware; - private DownstreamContext _downstreamContext; + private readonly DownstreamRouteFinderMiddleware _middleware; + private readonly DownstreamContext _downstreamContext; private OcelotRequestDelegate _next; private readonly Mock _multiplexer; public DownstreamRouteFinderMiddlewareTests() { - _provider = new Mock(); + _repo = new Mock(); _finder = new Mock(); _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); @@ -43,13 +42,13 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _multiplexer = new Mock(); - _middleware = new DownstreamRouteFinderMiddleware(_next, _loggerFactory.Object, _finder.Object, _provider.Object, _multiplexer.Object); + _middleware = new DownstreamRouteFinderMiddleware(_next, _loggerFactory.Object, _finder.Object, _repo.Object, _multiplexer.Object); } [Fact] public void should_call_scoped_data_repository_correctly() { - var config = new OcelotConfiguration(null, null, new ServiceProviderConfigurationBuilder().Build(), ""); + var config = new InternalConfiguration(null, null, new ServiceProviderConfigurationBuilder().Build(), ""); var downstreamReRoute = new DownstreamReRouteBuilder() .WithDownstreamPathTemplate("any old string") @@ -74,19 +73,19 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder _middleware.Invoke(_downstreamContext).GetAwaiter().GetType(); } - private void GivenTheFollowingConfig(IOcelotConfiguration config) + private void GivenTheFollowingConfig(IInternalConfiguration config) { _config = config; - _provider + _repo .Setup(x => x.Get()) - .ReturnsAsync(new OkResponse(_config)); + .Returns(new OkResponse(_config)); } private void GivenTheDownStreamRouteFinderReturns(DownstreamRoute downstreamRoute) { _downstreamRoute = new OkResponse(downstreamRoute); _finder - .Setup(x => x.FindDownstreamRoute(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(x => x.FindDownstreamRoute(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_downstreamRoute); } diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs index cb5c2a67..7406b1c5 100644 --- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs +++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs @@ -2,8 +2,6 @@ using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; -using Ocelot.Configuration.Creator; -using Ocelot.Configuration.Provider; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.UrlMatcher; @@ -23,7 +21,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder private string _upstreamUrlPath; private Response _result; private List _reRoutesConfig; - private OcelotConfiguration _config; + private InternalConfiguration _config; private Response _match; private string _upstreamHttpMethod; private string _upstreamHost; @@ -711,7 +709,7 @@ namespace Ocelot.UnitTests.DownstreamRouteFinder private void GivenTheConfigurationIs(List reRoutesConfig, string adminPath, ServiceProviderConfiguration serviceProviderConfig) { _reRoutesConfig = reRoutesConfig; - _config = new OcelotConfiguration(_reRoutesConfig, adminPath, serviceProviderConfig, ""); + _config = new InternalConfiguration(_reRoutesConfig, adminPath, serviceProviderConfig, ""); } private void GivenThereIsAnUpstreamUrlPath(string upstreamUrlPath) diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index c226f245..63797346 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -1,7 +1,4 @@ -using Ocelot.Configuration; -using Ocelot.Middleware; - -namespace Ocelot.UnitTests.DownstreamUrlCreator +namespace Ocelot.UnitTests.DownstreamUrlCreator { using System; using System.Collections.Generic; @@ -20,16 +17,20 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator using Xunit; using Shouldly; using Microsoft.AspNetCore.Http; + using Ocelot.Request.Middleware; + using Ocelot.Configuration; + using Ocelot.Middleware; public class DownstreamUrlCreatorMiddlewareTests { private readonly Mock _downstreamUrlTemplateVariableReplacer; private OkResponse _downstreamPath; - private Mock _loggerFactory; + private readonly Mock _loggerFactory; private Mock _logger; private DownstreamUrlCreatorMiddleware _middleware; - private DownstreamContext _downstreamContext; - private OcelotRequestDelegate _next; + private readonly DownstreamContext _downstreamContext; + private readonly OcelotRequestDelegate _next; + private readonly HttpRequestMessage _request; public DownstreamUrlCreatorMiddlewareTests() { @@ -38,7 +39,8 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _downstreamUrlTemplateVariableReplacer = new Mock(); - _downstreamContext.DownstreamRequest = new HttpRequestMessage(HttpMethod.Get, "https://my.url/abc/?q=123"); + _request = new HttpRequestMessage(HttpMethod.Get, "https://my.url/abc/?q=123"); + _downstreamContext.DownstreamRequest = new DownstreamRequest(_request); _next = context => Task.CompletedTask; } @@ -79,9 +81,9 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator .Build(); var config = new ServiceProviderConfigurationBuilder() - .WithServiceDiscoveryProviderType("ServiceFabric") - .WithServiceDiscoveryProviderHost("localhost") - .WithServiceDiscoveryProviderPort(19081) + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) .Build(); this.Given(x => x.GivenTheDownStreamRouteIs( @@ -115,9 +117,9 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator .Build()); var config = new ServiceProviderConfigurationBuilder() - .WithServiceDiscoveryProviderType("ServiceFabric") - .WithServiceDiscoveryProviderHost("localhost") - .WithServiceDiscoveryProviderPort(19081) + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) .Build(); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) @@ -145,9 +147,9 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator .Build()); var config = new ServiceProviderConfigurationBuilder() - .WithServiceDiscoveryProviderType("ServiceFabric") - .WithServiceDiscoveryProviderHost("localhost") - .WithServiceDiscoveryProviderPort(19081) + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) .Build(); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) @@ -175,9 +177,9 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator .Build()); var config = new ServiceProviderConfigurationBuilder() - .WithServiceDiscoveryProviderType("ServiceFabric") - .WithServiceDiscoveryProviderHost("localhost") - .WithServiceDiscoveryProviderPort(19081) + .WithType("ServiceFabric") + .WithHost("localhost") + .WithPort(19081) .Build(); this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) @@ -208,7 +210,8 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator private void GivenTheDownstreamRequestUriIs(string uri) { - _downstreamContext.DownstreamRequest.RequestUri = new Uri(uri); + _request.RequestUri = new Uri(uri); + _downstreamContext.DownstreamRequest = new DownstreamRequest(_request); } private void GivenTheUrlReplacerWillReturn(string path) @@ -221,7 +224,7 @@ namespace Ocelot.UnitTests.DownstreamUrlCreator private void ThenTheDownstreamRequestUriIs(string expectedUri) { - _downstreamContext.DownstreamRequest.RequestUri.OriginalString.ShouldBe(expectedUri); + _downstreamContext.DownstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); } } } diff --git a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs index 89eaaa38..8802a5bc 100644 --- a/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs @@ -1,5 +1,3 @@ -using Ocelot.Middleware; - namespace Ocelot.UnitTests.Errors { using System; @@ -11,27 +9,27 @@ namespace Ocelot.UnitTests.Errors using TestStack.BDDfy; using Xunit; using Microsoft.AspNetCore.Http; - using Ocelot.Configuration.Provider; using Moq; using Ocelot.Configuration; using Ocelot.Errors; - using Ocelot.DownstreamRouteFinder.Middleware; using Ocelot.Infrastructure.RequestData; + using Ocelot.Middleware; + using Ocelot.Configuration.Repository; public class ExceptionHandlerMiddlewareTests { - bool _shouldThrowAnException = false; - private Mock _provider; - private Mock _repo; + bool _shouldThrowAnException; + private readonly Mock _configRepo; + private readonly Mock _repo; private Mock _loggerFactory; private Mock _logger; - private ExceptionHandlerMiddleware _middleware; - private DownstreamContext _downstreamContext; + private readonly ExceptionHandlerMiddleware _middleware; + private readonly DownstreamContext _downstreamContext; private OcelotRequestDelegate _next; public ExceptionHandlerMiddlewareTests() { - _provider = new Mock(); + _configRepo = new Mock(); _repo = new Mock(); _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); @@ -47,26 +45,26 @@ namespace Ocelot.UnitTests.Errors context.HttpContext.Response.StatusCode = (int)HttpStatusCode.OK; }; - _middleware = new ExceptionHandlerMiddleware(_next, _loggerFactory.Object, _provider.Object, _repo.Object); + _middleware = new ExceptionHandlerMiddleware(_next, _loggerFactory.Object, _configRepo.Object, _repo.Object); } [Fact] public void NoDownstreamException() { - var config = new OcelotConfiguration(null, null, null, null); + var config = new InternalConfiguration(null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) .When(_ => WhenICallTheMiddleware()) .Then(_ => ThenTheResponseIsOk()) - .And(_ => TheRequestIdIsNotSet()) + .And(_ => TheAspDotnetRequestIdIsSet()) .BDDfy(); } [Fact] public void DownstreamException() { - var config = new OcelotConfiguration(null, null, null, null); + var config = new InternalConfiguration(null, null, null, null); this.Given(_ => GivenAnExceptionWillBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -78,7 +76,7 @@ namespace Ocelot.UnitTests.Errors [Fact] public void ShouldSetRequestId() { - var config = new OcelotConfiguration(null, null, null, "requestidkey"); + var config = new InternalConfiguration(null, null, null, "requestidkey"); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) @@ -89,15 +87,15 @@ namespace Ocelot.UnitTests.Errors } [Fact] - public void ShouldNotSetRequestId() + public void ShouldSetAspDotNetRequestId() { - var config = new OcelotConfiguration(null, null, null, null); + var config = new InternalConfiguration(null, null, null, null); this.Given(_ => GivenAnExceptionWillNotBeThrownDownstream()) .And(_ => GivenTheConfigurationIs(config)) .When(_ => WhenICallTheMiddlewareWithTheRequestIdKey("requestidkey", "1234")) .Then(_ => ThenTheResponseIsOk()) - .And(_ => TheRequestIdIsNotSet()) + .And(_ => TheAspDotnetRequestIdIsSet()) .BDDfy(); } @@ -135,8 +133,8 @@ namespace Ocelot.UnitTests.Errors private void GivenTheConfigThrows() { var ex = new Exception("outer", new Exception("inner")); - _provider - .Setup(x => x.Get()).ThrowsAsync(ex); + _configRepo + .Setup(x => x.Get()).Throws(ex); } private void ThenAnExceptionIsThrown() @@ -146,31 +144,21 @@ namespace Ocelot.UnitTests.Errors private void GivenTheConfigReturnsError() { - var config = new OcelotConfiguration(null, null, null, null); - - var response = new Ocelot.Responses.ErrorResponse(new FakeError()); - _provider - .Setup(x => x.Get()).ReturnsAsync(response); - } - - public class FakeError : Error - { - public FakeError() - : base("meh", OcelotErrorCode.CannotAddDataError) - { - } + var response = new Responses.ErrorResponse(new FakeError()); + _configRepo + .Setup(x => x.Get()).Returns(response); } private void TheRequestIdIsSet(string key, string value) { - _repo.Verify(x => x.Add(key, value), Times.Once); + _repo.Verify(x => x.Add(key, value), Times.Once); } - private void GivenTheConfigurationIs(IOcelotConfiguration config) + private void GivenTheConfigurationIs(IInternalConfiguration config) { - var response = new Ocelot.Responses.OkResponse(config); - _provider - .Setup(x => x.Get()).ReturnsAsync(response); + var response = new Responses.OkResponse(config); + _configRepo + .Setup(x => x.Get()).Returns(response); } private void GivenAnExceptionWillNotBeThrownDownstream() @@ -193,9 +181,17 @@ namespace Ocelot.UnitTests.Errors _downstreamContext.HttpContext.Response.StatusCode.ShouldBe(500); } - private void TheRequestIdIsNotSet() + private void TheAspDotnetRequestIdIsSet() { - _repo.Verify(x => x.Add(It.IsAny(), It.IsAny()), Times.Never); + _repo.Verify(x => x.Add(It.IsAny(), It.IsAny()), Times.Once); + } + + class FakeError : Error + { + internal FakeError() + : base("meh", OcelotErrorCode.CannotAddDataError) + { + } } } } diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs similarity index 92% rename from test/Ocelot.UnitTests/Headers/AddHeadersToRequestTests.cs rename to test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs index 0a8290bc..2ad59ca5 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs @@ -1,150 +1,151 @@ -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -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; -using System.Net.Http; - -namespace Ocelot.UnitTests.Headers -{ - public class AddHeadersToRequestTests - { - private readonly AddHeadersToRequest _addHeadersToRequest; - private readonly Mock _parser; - private readonly HttpRequestMessage _downstreamRequest; - private List _claims; - private List _configuration; - private Response _result; - private Response _claimValue; - - public AddHeadersToRequestTests() - { - _parser = new Mock(); - _addHeadersToRequest = new AddHeadersToRequest(_parser.Object); - _downstreamRequest = new HttpRequestMessage(); - } - - [Fact] - public void should_add_headers_to_downstreamRequest() - { - var claims = new List - { - new Claim("test", "data") - }; - - this.Given( - x => x.GivenConfigurationHeaderExtractorProperties(new List - { - new ClaimToThing("header-key", "", "", 0) - })) - .Given(x => x.GivenClaims(claims)) - .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) - .When(x => x.WhenIAddHeadersToTheRequest()) - .Then(x => x.ThenTheResultIsSuccess()) - .And(x => x.ThenTheHeaderIsAdded()) - .BDDfy(); - } - - [Fact] - public void should_replace_existing_headers_on_request() - { - this.Given( - x => x.GivenConfigurationHeaderExtractorProperties(new List - { - new ClaimToThing("header-key", "", "", 0) - })) - .Given(x => x.GivenClaims(new List - { - new Claim("test", "data") - })) - .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) - .And(x => x.GivenThatTheRequestContainsHeader("header-key", "initial")) - .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.GivenClaims(new List())) - .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List - { - new AnyError() - }))) - .When(x => x.WhenIAddHeadersToTheRequest()) - .Then(x => x.ThenTheResultIsError()) - .BDDfy(); - } - - private void GivenClaims(List claims) - { - _claims = claims; - } - - private void GivenConfigurationHeaderExtractorProperties(List configuration) - { - _configuration = configuration; - } - - private void GivenThatTheRequestContainsHeader(string key, string value) - { - _downstreamRequest.Headers.Add(key, value); - } - - 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.SetHeadersOnDownstreamRequest(_configuration, _claims, _downstreamRequest); - } - - private void ThenTheResultIsSuccess() - { - _result.IsError.ShouldBe(false); - } - - private void ThenTheResultIsError() - { - _result.IsError.ShouldBe(true); - } - - private void ThenTheHeaderIsAdded() - { - var header = _downstreamRequest.Headers.First(x => x.Key == "header-key"); - header.Value.First().ShouldBe(_claimValue.Data); - } - - class AnyError : Error - { - public AnyError() - : base("blahh", OcelotErrorCode.UnknownError) - { - } - } - } -} +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +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; +using System.Net.Http; +using Ocelot.Request.Middleware; + +namespace Ocelot.UnitTests.Headers +{ + public class AddHeadersToRequestClaimToThingTests + { + private readonly AddHeadersToRequest _addHeadersToRequest; + private readonly Mock _parser; + private readonly DownstreamRequest _downstreamRequest; + private List _claims; + private List _configuration; + private Response _result; + private Response _claimValue; + + public AddHeadersToRequestClaimToThingTests() + { + _parser = new Mock(); + _addHeadersToRequest = new AddHeadersToRequest(_parser.Object); + _downstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); + } + + [Fact] + public void should_add_headers_to_downstreamRequest() + { + var claims = new List + { + new Claim("test", "data") + }; + + this.Given( + x => x.GivenConfigurationHeaderExtractorProperties(new List + { + new ClaimToThing("header-key", "", "", 0) + })) + .Given(x => x.GivenClaims(claims)) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddHeadersToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenTheHeaderIsAdded()) + .BDDfy(); + } + + [Fact] + public void should_replace_existing_headers_on_request() + { + this.Given( + x => x.GivenConfigurationHeaderExtractorProperties(new List + { + new ClaimToThing("header-key", "", "", 0) + })) + .Given(x => x.GivenClaims(new List + { + new Claim("test", "data") + })) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .And(x => x.GivenThatTheRequestContainsHeader("header-key", "initial")) + .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.GivenClaims(new List())) + .And(x => x.GivenTheClaimParserReturns(new ErrorResponse(new List + { + new AnyError() + }))) + .When(x => x.WhenIAddHeadersToTheRequest()) + .Then(x => x.ThenTheResultIsError()) + .BDDfy(); + } + + private void GivenClaims(List claims) + { + _claims = claims; + } + + private void GivenConfigurationHeaderExtractorProperties(List configuration) + { + _configuration = configuration; + } + + private void GivenThatTheRequestContainsHeader(string key, string value) + { + _downstreamRequest.Headers.Add(key, value); + } + + 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.SetHeadersOnDownstreamRequest(_configuration, _claims, _downstreamRequest); + } + + private void ThenTheResultIsSuccess() + { + _result.IsError.ShouldBe(false); + } + + private void ThenTheResultIsError() + { + _result.IsError.ShouldBe(true); + } + + private void ThenTheHeaderIsAdded() + { + var header = _downstreamRequest.Headers.First(x => x.Key == "header-key"); + header.Value.First().ShouldBe(_claimValue.Data); + } + + class AnyError : Error + { + public AnyError() + : base("blahh", OcelotErrorCode.UnknownError) + { + } + } + } +} diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs new file mode 100644 index 00000000..69fd4f69 --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Configuration.Creator; +using Ocelot.Headers; +using Ocelot.Infrastructure.Claims.Parser; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Headers +{ + public class AddHeadersToRequestPlainTests + { + private readonly AddHeadersToRequest _addHeadersToRequest; + private HttpContext _context; + private AddHeader _addedHeader; + + public AddHeadersToRequestPlainTests() + { + _addHeadersToRequest = new AddHeadersToRequest(Mock.Of()); + } + + [Fact] + public void should_add_plain_text_header_to_downstream_request() + { + this.Given(_ => GivenHttpRequestWithoutHeaders()) + .When(_ => WhenAddingHeader("X-Custom-Header", "PlainValue")) + .Then(_ => ThenTheHeaderGetsTakenOverToTheRequestHeaders()) + .BDDfy(); + } + + [Fact] + public void should_overwrite_existing_header_with_added_header() + { + this.Given(_ => GivenHttpRequestWithHeader("X-Custom-Header", "This should get overwritten")) + .When(_ => WhenAddingHeader("X-Custom-Header", "PlainValue")) + .Then(_ => ThenTheHeaderGetsTakenOverToTheRequestHeaders()) + .BDDfy(); + } + + private void GivenHttpRequestWithoutHeaders() + { + _context = new DefaultHttpContext(); + } + + private void GivenHttpRequestWithHeader(string headerKey, string headerValue) + { + _context = new DefaultHttpContext + { + Request = + { + Headers = + { + { headerKey, headerValue } + } + } + }; + } + + private void WhenAddingHeader(string headerKey, string headerValue) + { + _addedHeader = new AddHeader(headerKey, headerValue); + _addHeadersToRequest.SetHeadersOnDownstreamRequest(new[] { _addedHeader }, _context); + } + + private void ThenTheHeaderGetsTakenOverToTheRequestHeaders() + { + var requestHeaders = _context.Request.Headers; + requestHeaders.ContainsKey(_addedHeader.Key).ShouldBeTrue($"Header {_addedHeader.Key} was expected but not there."); + var value = requestHeaders[_addedHeader.Key]; + value.ShouldNotBeNull($"Value of header {_addedHeader.Key} was expected to not be null."); + value.ToString().ShouldBe(_addedHeader.Value); + } + } +} diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs new file mode 100644 index 00000000..9c668dd1 --- /dev/null +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs @@ -0,0 +1,148 @@ +using Xunit; +using TestStack.BDDfy; +using Ocelot.Headers; +using System.Net.Http; +using System.Collections.Generic; +using System.Linq; +using Ocelot.Configuration.Creator; +using Moq; +using Ocelot.Responses; +using Ocelot.Infrastructure; +using Ocelot.UnitTests.Responder; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; +using Shouldly; + +namespace Ocelot.UnitTests.Headers +{ + public class AddHeadersToResponseTests + { + private readonly IAddHeadersToResponse _adder; + private readonly Mock _placeholders; + private DownstreamResponse _response; + private List _addHeaders; + private Mock _factory; + private readonly Mock _logger; + + public AddHeadersToResponseTests() + { + _factory = new Mock(); + _logger = new Mock(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _placeholders = new Mock(); + _adder = new AddHeadersToResponse(_placeholders.Object, _factory.Object); + } + + [Fact] + public void should_add_header() + { + var addHeaders = new List + { + new AddHeader("Laura", "Tom") + }; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .And(_ => ThenTheHeaderIsReturned("Laura", "Tom")) + .BDDfy(); + } + + [Fact] + public void should_add_trace_id_placeholder() + { + var addHeaders = new List + { + new AddHeader("Trace-Id", "{TraceId}") + }; + + var traceId = "123"; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheTraceIdIs(traceId)) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .Then(_ => ThenTheHeaderIsReturned("Trace-Id", traceId)) + .BDDfy(); + } + + [Fact] + public void should_add_trace_id_placeholder_and_normal() + { + var addHeaders = new List + { + new AddHeader("Trace-Id", "{TraceId}"), + new AddHeader("Tom", "Laura") + }; + + var traceId = "123"; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheTraceIdIs(traceId)) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .Then(_ => ThenTheHeaderIsReturned("Trace-Id", traceId)) + .Then(_ => ThenTheHeaderIsReturned("Tom", "Laura")) + .BDDfy(); + } + + [Fact] + public void should_do_nothing_and_log_error() + { + var addHeaders = new List + { + new AddHeader("Trace-Id", "{TraceId}") + }; + + this.Given(_ => GivenAResponseMessage()) + .And(_ => GivenTheTraceIdErrors()) + .And(_ => GivenTheAddHeaders(addHeaders)) + .When(_ => WhenIAdd()) + .Then(_ => ThenTheHeaderIsNotAdded("Trace-Id")) + .And(_ => ThenTheErrorIsLogged()) + .BDDfy(); + } + + private void ThenTheErrorIsLogged() + { + _logger.Verify(x => x.LogWarning("Unable to add header to response Trace-Id: {TraceId}"), Times.Once); + } + + private void ThenTheHeaderIsNotAdded(string key) + { + _response.Headers.Any(x => x.Key == key).ShouldBeFalse(); + } + + private void GivenTheTraceIdIs(string traceId) + { + _placeholders.Setup(x => x.Get("{TraceId}")).Returns(new OkResponse(traceId)); + } + + private void GivenTheTraceIdErrors() + { + _placeholders.Setup(x => x.Get("{TraceId}")).Returns(new ErrorResponse(new AnyError())); + } + + private void ThenTheHeaderIsReturned(string key, string value) + { + var values = _response.Headers.First(x => x.Key == key); + values.Values.First().ShouldBe(value); + } + + private void WhenIAdd() + { + _adder.Add(_addHeaders, _response); + } + + private void GivenAResponseMessage() + { + _response = new DownstreamResponse(new HttpResponseMessage()); + } + + private void GivenTheAddHeaders(List addHeaders) + { + _addHeaders = addHeaders; + } + } +} diff --git a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs index 3085a64d..7b13cc6d 100644 --- a/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs @@ -1,31 +1,34 @@ -using Xunit; -using Ocelot.Logging; -using Ocelot.Headers.Middleware; -using TestStack.BDDfy; -using Microsoft.AspNetCore.Http; -using System.Collections.Generic; -using Moq; -using Ocelot.Configuration; -using Ocelot.DownstreamRouteFinder; -using Ocelot.Configuration.Builder; -using Ocelot.Headers; -using System.Net.Http; -using Ocelot.Authorisation.Middleware; -using Ocelot.Middleware; - namespace Ocelot.UnitTests.Headers { + using Xunit; + using Ocelot.Logging; + using Ocelot.Headers.Middleware; + using TestStack.BDDfy; + using Microsoft.AspNetCore.Http; + using System.Collections.Generic; + using Moq; + using Ocelot.Configuration; + using Ocelot.DownstreamRouteFinder; + using Ocelot.Configuration.Builder; + using Ocelot.Headers; + using System.Net.Http; + using Ocelot.Authorisation.Middleware; + using Ocelot.Middleware; + using Ocelot.Middleware.Multiplexer; using System.Threading.Tasks; + using Ocelot.Request.Middleware; public class HttpHeadersTransformationMiddlewareTests { - private Mock _preReplacer; - private Mock _postReplacer; + private readonly Mock _preReplacer; + private readonly Mock _postReplacer; private Mock _loggerFactory; private Mock _logger; - private HttpHeadersTransformationMiddleware _middleware; - private DownstreamContext _downstreamContext; + private readonly HttpHeadersTransformationMiddleware _middleware; + private readonly DownstreamContext _downstreamContext; private OcelotRequestDelegate _next; + private readonly Mock _addHeadersToResponse; + private readonly Mock _addHeadersToRequest; public HttpHeadersTransformationMiddlewareTests() { @@ -36,7 +39,11 @@ namespace Ocelot.UnitTests.Headers _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; - _middleware = new HttpHeadersTransformationMiddleware(_next, _loggerFactory.Object, _preReplacer.Object, _postReplacer.Object); + _addHeadersToResponse = new Mock(); + _addHeadersToRequest = new Mock(); + _middleware = new HttpHeadersTransformationMiddleware( + _next, _loggerFactory.Object, _preReplacer.Object, + _postReplacer.Object, _addHeadersToResponse.Object, _addHeadersToRequest.Object); } [Fact] @@ -48,10 +55,24 @@ namespace Ocelot.UnitTests.Headers .And(x => GivenTheHttpResponseMessageIs()) .When(x => WhenICallTheMiddleware()) .Then(x => ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly()) + .Then(x => ThenAddHeadersToRequestIsCalledCorrectly()) .And(x => ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly()) + .And(x => ThenAddHeadersToResponseIsCalledCorrectly()) .BDDfy(); } + private void ThenAddHeadersToResponseIsCalledCorrectly() + { + _addHeadersToResponse + .Verify(x => x.Add(_downstreamContext.DownstreamReRoute.AddHeadersToDownstream, _downstreamContext.DownstreamResponse), Times.Once); + } + + private void ThenAddHeadersToRequestIsCalledCorrectly() + { + _addHeadersToRequest + .Verify(x => x.SetHeadersOnDownstreamRequest(_downstreamContext.DownstreamReRoute.AddHeadersToUpstream, _downstreamContext.HttpContext), Times.Once); + } + private void WhenICallTheMiddleware() { _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); @@ -59,12 +80,12 @@ namespace Ocelot.UnitTests.Headers private void GivenTheDownstreamRequestIs() { - _downstreamContext.DownstreamRequest = new HttpRequestMessage(); + _downstreamContext.DownstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); } private void GivenTheHttpResponseMessageIs() { - _downstreamContext.DownstreamResponse = new HttpResponseMessage(); + _downstreamContext.DownstreamResponse = new DownstreamResponse(new HttpResponseMessage()); } private void GivenTheReRouteHasPreFindAndReplaceSetUp() @@ -88,7 +109,7 @@ namespace Ocelot.UnitTests.Headers private void ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly() { - _postReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>(), It.IsAny()), Times.Once); + _postReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>(), It.IsAny()), Times.Once); } private void GivenTheFollowingRequest() diff --git a/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs index 7f96b247..7a448118 100644 --- a/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpRequestHeadersBuilderMiddlewareTests.cs @@ -14,6 +14,7 @@ namespace Ocelot.UnitTests.Headers using Ocelot.Headers; using Ocelot.Headers.Middleware; using Ocelot.Logging; + using Ocelot.Request.Middleware; using Ocelot.Responses; using TestStack.BDDfy; using Xunit; @@ -37,7 +38,7 @@ namespace Ocelot.UnitTests.Headers _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _middleware = new HttpRequestHeadersBuilderMiddleware(_next, _loggerFactory.Object, _addHeaders.Object); - _downstreamContext.DownstreamRequest = new HttpRequestMessage(); + _downstreamContext.DownstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); } [Fact] @@ -81,7 +82,7 @@ namespace Ocelot.UnitTests.Headers .Setup(x => x.SetHeadersOnDownstreamRequest( It.IsAny>(), It.IsAny>(), - It.IsAny())) + It.IsAny())) .Returns(new OkResponse()); } diff --git a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs index 95ae6e6d..3e7bc077 100644 --- a/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs +++ b/test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs @@ -7,30 +7,45 @@ using Ocelot.Configuration; using System.Collections.Generic; using Ocelot.Responses; using System.Linq; +using System.Net; +using Moq; +using Ocelot.Infrastructure; +using Ocelot.Middleware; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Request.Middleware; namespace Ocelot.UnitTests.Headers { public class HttpResponseHeaderReplacerTests { - private HttpResponseMessage _response; - private HttpResponseHeaderReplacer _replacer; + private DownstreamResponse _response; + private Placeholders _placeholders; + private readonly HttpResponseHeaderReplacer _replacer; private List _headerFindAndReplaces; private Response _result; - private HttpRequestMessage _request; + private DownstreamRequest _request; + private Mock _finder; + private Mock _repo; public HttpResponseHeaderReplacerTests() { - _replacer = new HttpResponseHeaderReplacer(); + _repo = new Mock(); + _finder = new Mock(); + _placeholders = new Placeholders(_finder.Object, _repo.Object); + _replacer = new HttpResponseHeaderReplacer(_placeholders); } [Fact] public void should_replace_headers() { - var response = new HttpResponseMessage(); - response.Headers.Add("test", "test"); + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, + new List>>() + { + new KeyValuePair>("test", new List {"test"}) + }); - var fAndRs = new List(); - fAndRs.Add(new HeaderFindAndReplace("test", "test", "chiken", 0)); + var fAndRs = new List {new HeaderFindAndReplace("test", "test", "chiken", 0)}; this.Given(x => GivenTheHttpResponse(response)) .And(x => GivenTheFollowingHeaderReplacements(fAndRs)) @@ -42,8 +57,11 @@ namespace Ocelot.UnitTests.Headers [Fact] public void should_not_replace_headers() { - var response = new HttpResponseMessage(); - response.Headers.Add("test", "test"); + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, + new List>>() + { + new KeyValuePair>("test", new List {"test"}) + }); var fAndRs = new List(); @@ -57,16 +75,21 @@ namespace Ocelot.UnitTests.Headers [Fact] public void should_replace_downstream_base_url_with_ocelot_base_url() { - var downstreamUrl = "http://downstream.com/"; + const string downstreamUrl = "http://downstream.com/"; - var request = new HttpRequestMessage(); - request.RequestUri = new System.Uri(downstreamUrl); + var request = + new HttpRequestMessage(HttpMethod.Get, "http://test.com") {RequestUri = new System.Uri(downstreamUrl)}; - var response = new HttpResponseMessage(); - response.Headers.Add("Location", downstreamUrl); + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, + new List>>() + { + new KeyValuePair>("Location", new List {downstreamUrl}) + }); - var fAndRs = new List(); - fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0)); + var fAndRs = new List + { + new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0) + }; this.Given(x => GivenTheHttpResponse(response)) .And(x => GivenTheRequestIs(request)) @@ -79,16 +102,21 @@ namespace Ocelot.UnitTests.Headers [Fact] public void should_replace_downstream_base_url_with_ocelot_base_url_with_port() { - var downstreamUrl = "http://downstream.com/"; + const string downstreamUrl = "http://downstream.com/"; - var request = new HttpRequestMessage(); - request.RequestUri = new System.Uri(downstreamUrl); + var request = + new HttpRequestMessage(HttpMethod.Get, "http://test.com") {RequestUri = new System.Uri(downstreamUrl)}; - var response = new HttpResponseMessage(); - response.Headers.Add("Location", downstreamUrl); + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, + new List>>() + { + new KeyValuePair>("Location", new List {downstreamUrl}) + }); - var fAndRs = new List(); - fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0)); + var fAndRs = new List + { + new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0) + }; this.Given(x => GivenTheHttpResponse(response)) .And(x => GivenTheRequestIs(request)) @@ -101,16 +129,21 @@ namespace Ocelot.UnitTests.Headers [Fact] public void should_replace_downstream_base_url_with_ocelot_base_url_and_path() { - var downstreamUrl = "http://downstream.com/test/product"; + const string downstreamUrl = "http://downstream.com/test/product"; - var request = new HttpRequestMessage(); - request.RequestUri = new System.Uri(downstreamUrl); + var request = + new HttpRequestMessage(HttpMethod.Get, "http://test.com") {RequestUri = new System.Uri(downstreamUrl)}; - var response = new HttpResponseMessage(); - response.Headers.Add("Location", downstreamUrl); + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, + new List>>() + { + new KeyValuePair>("Location", new List {downstreamUrl}) + }); - var fAndRs = new List(); - fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0)); + var fAndRs = new List + { + new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0) + }; this.Given(x => GivenTheHttpResponse(response)) .And(x => GivenTheRequestIs(request)) @@ -123,16 +156,21 @@ namespace Ocelot.UnitTests.Headers [Fact] public void should_replace_downstream_base_url_with_ocelot_base_url_with_path_and_port() { - var downstreamUrl = "http://downstream.com/test/product"; + const string downstreamUrl = "http://downstream.com/test/product"; - var request = new HttpRequestMessage(); - request.RequestUri = new System.Uri(downstreamUrl); + var request = + new HttpRequestMessage(HttpMethod.Get, "http://test.com") {RequestUri = new System.Uri(downstreamUrl)}; - var response = new HttpResponseMessage(); - response.Headers.Add("Location", downstreamUrl); + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, + new List>>() + { + new KeyValuePair>("Location", new List {downstreamUrl}) + }); - var fAndRs = new List(); - fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0)); + var fAndRs = new List + { + new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0) + }; this.Given(x => GivenTheHttpResponse(response)) .And(x => GivenTheRequestIs(request)) @@ -145,16 +183,21 @@ namespace Ocelot.UnitTests.Headers [Fact] public void should_replace_downstream_base_url_and_port_with_ocelot_base_url() { - var downstreamUrl = "http://downstream.com:123/test/product"; + const string downstreamUrl = "http://downstream.com:123/test/product"; - var request = new HttpRequestMessage(); - request.RequestUri = new System.Uri(downstreamUrl); + var request = + new HttpRequestMessage(HttpMethod.Get, "http://test.com") {RequestUri = new System.Uri(downstreamUrl)}; - var response = new HttpResponseMessage(); - response.Headers.Add("Location", downstreamUrl); + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, + new List>>() + { + new KeyValuePair>("Location", new List {downstreamUrl}) + }); - var fAndRs = new List(); - fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0)); + var fAndRs = new List + { + new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0) + }; this.Given(x => GivenTheHttpResponse(response)) .And(x => GivenTheRequestIs(request)) @@ -167,16 +210,21 @@ namespace Ocelot.UnitTests.Headers [Fact] public void should_replace_downstream_base_url_and_port_with_ocelot_base_url_and_port() { - var downstreamUrl = "http://downstream.com:123/test/product"; + const string downstreamUrl = "http://downstream.com:123/test/product"; - var request = new HttpRequestMessage(); - request.RequestUri = new System.Uri(downstreamUrl); + var request = + new HttpRequestMessage(HttpMethod.Get, "http://test.com") {RequestUri = new System.Uri(downstreamUrl)}; - var response = new HttpResponseMessage(); - response.Headers.Add("Location", downstreamUrl); + var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, + new List>>() + { + new KeyValuePair>("Location", new List {downstreamUrl}) + }); - var fAndRs = new List(); - fAndRs.Add(new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:321/", 0)); + var fAndRs = new List + { + new HeaderFindAndReplace("Location", "{DownstreamBaseUrl}", "http://ocelot.com:321/", 0) + }; this.Given(x => GivenTheHttpResponse(response)) .And(x => GivenTheRequestIs(request)) @@ -188,7 +236,7 @@ namespace Ocelot.UnitTests.Headers private void GivenTheRequestIs(HttpRequestMessage request) { - _request = request; + _request = new DownstreamRequest(request); } private void ThenTheHeadersAreNotReplaced() @@ -196,8 +244,8 @@ namespace Ocelot.UnitTests.Headers _result.ShouldBeOfType(); foreach (var f in _headerFindAndReplaces) { - _response.Headers.TryGetValues(f.Key, out var values); - values.ToList()[f.Index].ShouldBe("test"); + var values = _response.Headers.First(x => x.Key == f.Key); + values.Values.ToList()[f.Index].ShouldBe("test"); } } @@ -206,7 +254,7 @@ namespace Ocelot.UnitTests.Headers _headerFindAndReplaces = fAndRs; } - private void GivenTheHttpResponse(HttpResponseMessage response) + private void GivenTheHttpResponse(DownstreamResponse response) { _response = response; } @@ -218,17 +266,17 @@ namespace Ocelot.UnitTests.Headers private void ThenTheHeaderShouldBe(string key, string value) { - var test = _response.Headers.GetValues(key); - test.First().ShouldBe(value); + var test = _response.Headers.First(x => x.Key == key); + test.Values.First().ShouldBe(value); } - private void ThenTheHeadersAreReplaced() + private void ThenTheHeadersAreReplaced() { _result.ShouldBeOfType(); foreach (var f in _headerFindAndReplaces) { - _response.Headers.TryGetValues(f.Key, out var values); - values.ToList()[f.Index].ShouldBe(f.Replace); + var values = _response.Headers.First(x => x.Key == f.Key); + values.Values.ToList()[f.Index].ShouldBe(f.Replace); } } } diff --git a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs index 35c792aa..9818c944 100644 --- a/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs +++ b/test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs @@ -1,5 +1,5 @@ -using System.Net.Http; -using System.Net.Http.Headers; +using System.Collections.Generic; +using Ocelot.Middleware; using Ocelot.Responses; using Shouldly; using TestStack.BDDfy; @@ -9,7 +9,7 @@ namespace Ocelot.UnitTests.Headers { public class RemoveHeadersTests { - private HttpResponseHeaders _headers; + private List
_headers; private readonly Ocelot.Headers.RemoveOutputHeaders _removeOutputHeaders; private Response _result; @@ -21,18 +21,18 @@ namespace Ocelot.UnitTests.Headers [Fact] public void should_remove_header() { - var httpResponse = new HttpResponseMessage() + var headers = new List
() { - Headers = {{ "Transfer-Encoding", "chunked"}} + new Header("Transfer-Encoding", new List {"chunked"}) }; - this.Given(x => x.GivenAHttpContext(httpResponse.Headers)) + this.Given(x => x.GivenAHttpContext(headers)) .When(x => x.WhenIRemoveTheHeaders()) .Then(x => x.TheHeaderIsNoLongerInTheContext()) .BDDfy(); } - private void GivenAHttpContext(HttpResponseHeaders headers) + private void GivenAHttpContext(List
headers) { _headers = headers; } diff --git a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs index c9f66009..555cfe32 100644 --- a/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs +++ b/test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs @@ -9,9 +9,9 @@ namespace Ocelot.UnitTests.Infrastructure { public class HttpDataRepositoryTests { - private HttpContext _httpContext; + private readonly HttpContext _httpContext; private IHttpContextAccessor _httpContextAccessor; - private HttpDataRepository _httpDataRepository; + private readonly HttpDataRepository _httpDataRepository; private object _result; public HttpDataRepositoryTests() diff --git a/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs b/test/Ocelot.UnitTests/Infrastructure/IScopedRequestDataRepository.cs new file mode 100644 index 00000000..e69de29b diff --git a/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs b/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs new file mode 100644 index 00000000..729c4329 --- /dev/null +++ b/test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs @@ -0,0 +1,82 @@ +using System; +using System.Net.Http; +using Moq; +using Ocelot.Infrastructure; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; +using Ocelot.Responses; +using Shouldly; +using Xunit; + +namespace Ocelot.UnitTests.Infrastructure +{ + public class PlaceholdersTests + { + private IPlaceholders _placeholders; + private Mock _finder; + private Mock _repo; + + public PlaceholdersTests() + { + _repo = new Mock(); + _finder = new Mock(); + _placeholders = new Placeholders(_finder.Object, _repo.Object); + } + + [Fact] + public void should_return_base_url() + { + var baseUrl = "http://www.bbc.co.uk"; + _finder.Setup(x => x.Find()).Returns(baseUrl); + var result = _placeholders.Get("{BaseUrl}"); + result.Data.ShouldBe(baseUrl); + } + + [Fact] + public void should_return_key_does_not_exist() + { + var result = _placeholders.Get("{Test}"); + result.IsError.ShouldBeTrue(); + result.Errors[0].Message.ShouldBe("Unable to find placeholder called {Test}"); + } + + [Fact] + public void should_return_downstream_base_url_when_port_is_not_80_or_443() + { + var httpRequest = new HttpRequestMessage(); + httpRequest.RequestUri = new Uri("http://www.bbc.co.uk"); + var request = new DownstreamRequest(httpRequest); + var result = _placeholders.Get("{DownstreamBaseUrl}", request); + result.Data.ShouldBe("http://www.bbc.co.uk/"); + } + + [Fact] + public void should_return_downstream_base_url_when_port_is_80_or_443() + { + var httpRequest = new HttpRequestMessage(); + httpRequest.RequestUri = new Uri("http://www.bbc.co.uk:123"); + var request = new DownstreamRequest(httpRequest); + var result = _placeholders.Get("{DownstreamBaseUrl}", request); + result.Data.ShouldBe("http://www.bbc.co.uk:123/"); + } + + [Fact] + public void should_return_key_does_not_exist_for_http_request_message() + { + var request = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://west.com")); + var result = _placeholders.Get("{Test}", request); + result.IsError.ShouldBeTrue(); + result.Errors[0].Message.ShouldBe("Unable to find placeholder called {Test}"); + } + + [Fact] + public void should_return_trace_id() + { + var traceId = "123"; + _repo.Setup(x => x.Get("TraceId")).Returns(new OkResponse(traceId)); + var result = _placeholders.Get("{TraceId}"); + result.Data.ShouldBe(traceId); + } + } +} diff --git a/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs b/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs new file mode 100644 index 00000000..88632675 --- /dev/null +++ b/test/Ocelot.UnitTests/Infrastructure/StringExtensionsTests.cs @@ -0,0 +1,29 @@ +using Xunit; +using Ocelot.Infrastructure.Extensions; +using Shouldly; + +namespace Ocelot.UnitTests.Infrastructure +{ + public class StringExtensionsTests + { + [Fact] + public void should_trim_start() + { + var test = "/string"; + + test = test.TrimStart("/"); + + test.ShouldBe("string"); + } + + [Fact] + public void should_return_source() + { + var test = "string"; + + test = test.LastCharAsForwardSlash(); + + test.ShouldBe("string/"); + } + } +} diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs index 5d7fa8b6..da439976 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs @@ -5,6 +5,7 @@ using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.ServiceDiscovery; using Shouldly; using System.Collections.Generic; +using Ocelot.ServiceDiscovery.Providers; using TestStack.BDDfy; using Xunit; diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs index 1e30a0d1..cdd63d5a 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs @@ -25,7 +25,7 @@ namespace Ocelot.UnitTests.LoadBalancer { _factory = new Mock(); _loadBalancerHouse = new LoadBalancerHouse(_factory.Object); - _serviceProviderConfig = new ServiceProviderConfiguration("myType","myHost",123); + _serviceProviderConfig = new ServiceProviderConfiguration("myType","myHost",123, string.Empty); } [Fact] diff --git a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs index e03fe6e1..a51b5cff 100644 --- a/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs @@ -13,6 +13,7 @@ namespace Ocelot.UnitTests.LoadBalancer using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.LoadBalancer.Middleware; using Ocelot.Logging; + using Ocelot.Request.Middleware; using Ocelot.Responses; using Ocelot.Values; using Shouldly; @@ -39,13 +40,13 @@ namespace Ocelot.UnitTests.LoadBalancer _loadBalancerHouse = new Mock(); _loadBalancer = new Mock(); _loadBalancerHouse = new Mock(); - _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, ""); + _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, "http://test.com/"); _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; - _downstreamContext.DownstreamRequest = _downstreamRequest; + _downstreamContext.DownstreamRequest = new DownstreamRequest(_downstreamRequest); } [Fact] @@ -122,6 +123,7 @@ namespace Ocelot.UnitTests.LoadBalancer private void GivenTheDownStreamUrlIs(string downstreamUrl) { _downstreamRequest.RequestUri = new System.Uri(downstreamUrl); + _downstreamContext.DownstreamRequest = new DownstreamRequest(_downstreamRequest); } private void GivenTheLoadBalancerReturnsAnError() @@ -185,7 +187,7 @@ namespace Ocelot.UnitTests.LoadBalancer private void ThenTheDownstreamUrlIsReplacedWith(string expectedUri) { - _downstreamContext.DownstreamRequest.RequestUri.OriginalString.ShouldBe(expectedUri); + _downstreamContext.DownstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); } } } diff --git a/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs b/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs new file mode 100644 index 00000000..8a1fa891 --- /dev/null +++ b/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs @@ -0,0 +1,80 @@ +namespace Ocelot.UnitTests.Logging +{ + using Moq; + using Xunit; + using Ocelot.Logging; + using Microsoft.Extensions.Logging; + using Ocelot.Infrastructure.RequestData; + using System; + + public class AspDotNetLoggerTests + { + private readonly Mock> _coreLogger; + private readonly AspDotNetLogger _logger; + private Mock _repo; + private readonly string _b; + private readonly string _a; + private readonly Exception _ex; + + public AspDotNetLoggerTests() + { + _a = "tom"; + _b = "laura"; + _ex = new Exception("oh no"); + _coreLogger = new Mock>(); + _repo = new Mock(); + _logger = new AspDotNetLogger(_coreLogger.Object, _repo.Object); + } + + [Fact] + public void should_log_trace() + { + _logger.LogTrace($"a message from {_a} to {_b}"); + + ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Trace); + } + + [Fact] + public void should_log_info() + { + _logger.LogInformation($"a message from {_a} to {_b}"); + + ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Information); + } + + [Fact] + public void should_log_warning() + { + _logger.LogWarning($"a message from {_a} to {_b}"); + + ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Warning); + } + + [Fact] + public void should_log_error() + { + _logger.LogError($"a message from {_a} to {_b}", _ex); + + ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura, exception: System.Exception: oh no", LogLevel.Error); + } + + [Fact] + public void should_log_critical() + { + _logger.LogCritical($"a message from {_a} to {_b}", _ex); + + ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura, exception: System.Exception: oh no", LogLevel.Critical); + } + + private void ThenLevelIsLogged(string expected, LogLevel expectedLogLevel) + { + _coreLogger.Verify( + x => x.Log( + expectedLogLevel, + It.IsAny(), + It.Is(o => o.ToString() == expected), + It.IsAny(), + It.IsAny>()), Times.Once); + } + } +} diff --git a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs new file mode 100644 index 00000000..3703ddb0 --- /dev/null +++ b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs @@ -0,0 +1,145 @@ +using Ocelot.Logging; +using Moq; +using TestStack.BDDfy; +using Butterfly.Client.Tracing; +using Ocelot.Requester; +using Xunit; +using Ocelot.Middleware; +using Microsoft.AspNetCore.Http; +using System; + +namespace Ocelot.UnitTests.Logging +{ + public class OcelotDiagnosticListenerTests + { + private readonly OcelotDiagnosticListener _listener; + private Mock _factory; + private readonly Mock _logger; + private IServiceTracer _tracer; + private DownstreamContext _downstreamContext; + private string _name; + private Exception _exception; + + public OcelotDiagnosticListenerTests() + { + _factory = new Mock(); + _logger = new Mock(); + _tracer = new FakeServiceTracer(); + _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); + _listener = new OcelotDiagnosticListener(_factory.Object, _tracer); + } + + [Fact] + public void should_trace_ocelot_middleware_started() + { + this.Given(_ => GivenAMiddlewareName()) + .And(_ => GivenAContext()) + .When(_ => WhenOcelotMiddlewareStartedCalled()) + .Then(_ => ThenTheLogIs($"Ocelot.MiddlewareStarted: {_name}; {_downstreamContext.HttpContext.Request.Path}")) + .BDDfy(); + } + + [Fact] + public void should_trace_ocelot_middleware_finished() + { + this.Given(_ => GivenAMiddlewareName()) + .And(_ => GivenAContext()) + .When(_ => WhenOcelotMiddlewareFinishedCalled()) + .Then(_ => ThenTheLogIs($"Ocelot.MiddlewareFinished: {_name}; {_downstreamContext.HttpContext.Request.Path}")) + .BDDfy(); + } + + [Fact] + public void should_trace_ocelot_middleware_exception() + { + this.Given(_ => GivenAMiddlewareName()) + .And(_ => GivenAContext()) + .And(_ => GivenAException(new Exception("oh no"))) + .When(_ => WhenOcelotMiddlewareExceptionCalled()) + .Then(_ => ThenTheLogIs($"Ocelot.MiddlewareException: {_name}; {_exception.Message};")) + .BDDfy(); + } + + [Fact] + public void should_trace_middleware_started() + { + this.Given(_ => GivenAMiddlewareName()) + .And(_ => GivenAContext()) + .When(_ => WhenMiddlewareStartedCalled()) + .Then(_ => ThenTheLogIs($"MiddlewareStarting: {_name}; {_downstreamContext.HttpContext.Request.Path}")) + .BDDfy(); + } + + [Fact] + public void should_trace_middleware_finished() + { + this.Given(_ => GivenAMiddlewareName()) + .And(_ => GivenAContext()) + .When(_ => WhenMiddlewareFinishedCalled()) + .Then(_ => ThenTheLogIs($"MiddlewareFinished: {_name}; {_downstreamContext.HttpContext.Response.StatusCode}")) + .BDDfy(); + } + + [Fact] + public void should_trace_middleware_exception() + { + this.Given(_ => GivenAMiddlewareName()) + .And(_ => GivenAContext()) + .And(_ => GivenAException(new Exception("oh no"))) + .When(_ => WhenMiddlewareExceptionCalled()) + .Then(_ => ThenTheLogIs($"MiddlewareException: {_name}; {_exception.Message};")) + .BDDfy(); + } + + private void GivenAException(Exception exception) + { + _exception = exception; + } + + private void WhenOcelotMiddlewareStartedCalled() + { + _listener.OcelotMiddlewareStarted(_downstreamContext, _name); + } + + private void WhenOcelotMiddlewareFinishedCalled() + { + _listener.OcelotMiddlewareFinished(_downstreamContext, _name); + } + + private void WhenOcelotMiddlewareExceptionCalled() + { + _listener.OcelotMiddlewareException(_exception, _downstreamContext, _name); + } + + private void WhenMiddlewareStartedCalled() + { + _listener.OnMiddlewareStarting(_downstreamContext.HttpContext, _name); + } + + private void WhenMiddlewareFinishedCalled() + { + _listener.OnMiddlewareFinished(_downstreamContext.HttpContext, _name); + } + + private void WhenMiddlewareExceptionCalled() + { + _listener.OnMiddlewareException(_exception, _name); + } + + private void GivenAContext() + { + _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + } + + private void GivenAMiddlewareName() + { + _name = "name"; + } + + private void ThenTheLogIs(string expected) + { + _logger.Verify( + x => x.LogTrace(expected)); + } + } +} diff --git a/test/Ocelot.UnitTests/Middleware/DefinedAggregatorProviderTests.cs b/test/Ocelot.UnitTests/Middleware/DefinedAggregatorProviderTests.cs new file mode 100644 index 00000000..1874e94b --- /dev/null +++ b/test/Ocelot.UnitTests/Middleware/DefinedAggregatorProviderTests.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Responses; +using Shouldly; +using TestStack.BDDfy; +using Xunit; +using static Ocelot.UnitTests.Middleware.UserDefinedResponseAggregatorTests; + +namespace Ocelot.UnitTests.Middleware +{ + public class DefinedAggregatorProviderTests + { + private ServiceLocatorDefinedAggregatorProvider _provider; + private Response _aggregator; + private ReRoute _reRoute; + + [Fact] + public void should_find_aggregator() + { + var reRoute = new ReRouteBuilder() + .WithAggregator("TestDefinedAggregator") + .Build(); + + this.Given(_ => GivenDefinedAggregator()) + .And(_ => GivenReRoute(reRoute)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheAggregatorIsReturned()) + .BDDfy(); + } + + [Fact] + public void should_not_find_aggregator() + { + var reRoute = new ReRouteBuilder() + .WithAggregator("TestDefinedAggregator") + .Build(); + + this.Given(_ => GivenNoDefinedAggregator()) + .And(_ => GivenReRoute(reRoute)) + .When(_ => WhenIGet()) + .Then(_ => ThenAnErrorIsReturned()) + .BDDfy(); + } + + private void GivenDefinedAggregator() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(); + var services = serviceCollection.BuildServiceProvider(); + _provider = new ServiceLocatorDefinedAggregatorProvider(services); + } + + private void ThenTheAggregatorIsReturned() + { + _aggregator.Data.ShouldNotBeNull(); + _aggregator.Data.ShouldBeOfType(); + _aggregator.IsError.ShouldBeFalse(); + } + + private void GivenNoDefinedAggregator() + { + var serviceCollection = new ServiceCollection(); + var services = serviceCollection.BuildServiceProvider(); + _provider = new ServiceLocatorDefinedAggregatorProvider(services); + } + + private void GivenReRoute(ReRoute reRoute) + { + _reRoute = reRoute; + } + + private void WhenIGet() + { + _aggregator = _provider.Get(_reRoute); + } + + private void ThenAnErrorIsReturned() + { + _aggregator.IsError.ShouldBeTrue(); + _aggregator.Errors[0].Message.ShouldBe("Could not find Aggregator: TestDefinedAggregator"); + _aggregator.Errors[0].ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/Middleware/MultiplexerTests.cs b/test/Ocelot.UnitTests/Middleware/MultiplexerTests.cs index 1584c987..787bfefa 100644 --- a/test/Ocelot.UnitTests/Middleware/MultiplexerTests.cs +++ b/test/Ocelot.UnitTests/Middleware/MultiplexerTests.cs @@ -19,13 +19,16 @@ namespace Ocelot.UnitTests.Middleware private readonly OcelotRequestDelegate _pipeline; private int _count; private Mock _aggregator; + private Mock _factory; public MultiplexerTests() { + _factory = new Mock(); _aggregator = new Mock(); _context = new DownstreamContext(new DefaultHttpContext()); _pipeline = context => Task.FromResult(_count++); - _multiplexer = new Multiplexer(_aggregator.Object); + _factory.Setup(x => x.Get(It.IsAny())).Returns(_aggregator.Object); + _multiplexer = new Multiplexer(_factory.Object); } [Fact] diff --git a/test/Ocelot.UnitTests/Middleware/OcelotMiddlewareTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotMiddlewareTests.cs new file mode 100644 index 00000000..9e13ed1a --- /dev/null +++ b/test/Ocelot.UnitTests/Middleware/OcelotMiddlewareTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Errors; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.UnitTests.Responder; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Middleware +{ + public class OcelotMiddlewareTests + { + private Mock _logger; + private FakeMiddleware _middleware; + private List _errors; + + public OcelotMiddlewareTests() + { + _errors = new List(); + _logger = new Mock(); + _middleware = new FakeMiddleware(_logger.Object); + } + + [Fact] + public void should_log_error() + { + this.Given(x => GivenAnError(new AnyError())) + .When(x => WhenISetTheError()) + .Then(x => ThenTheErrorIsLogged(1)) + .BDDfy(); + } + + [Fact] + public void should_log_errors() + { + this.Given(x => GivenAnError(new AnyError())) + .And(x => GivenAnError(new AnyError())) + .When(x => WhenISetTheErrors()) + .Then(x => ThenTheErrorIsLogged(2)) + .BDDfy(); + } + + private void WhenISetTheErrors() + { + _middleware.SetPipelineError(new DownstreamContext(new DefaultHttpContext()), _errors); + } + + private void ThenTheErrorIsLogged(int times) + { + _logger.Verify(x => x.LogWarning("blahh"), Times.Exactly(times)); + } + + private void WhenISetTheError() + { + _middleware.SetPipelineError(new DownstreamContext(new DefaultHttpContext()), _errors[0]); + } + + private void GivenAnError(Error error) + { + _errors.Add(error); + } + } + + public class FakeMiddleware : OcelotMiddleware + { + public FakeMiddleware(IOcelotLogger logger) + : base(logger) + { + } + } +} diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs new file mode 100644 index 00000000..17f9e3e8 --- /dev/null +++ b/test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs @@ -0,0 +1,49 @@ +namespace Ocelot.UnitTests.Middleware +{ + using Microsoft.Extensions.Configuration; + using Microsoft.Extensions.DependencyInjection; + using Ocelot.DependencyInjection; + using Ocelot.Middleware; + using Ocelot.Middleware.Pipeline; + using Pivotal.Discovery.Client; + using Shouldly; + using TestStack.BDDfy; + using Xunit; + + public class OcelotPipelineExtensionsTests + { + private OcelotPipelineBuilder _builder; + private OcelotRequestDelegate _handlers; + + [Fact] + public void should_set_up_pipeline() + { + this.Given(_ => GivenTheDepedenciesAreSetUp()) + .When(_ => WhenIBuild()) + .Then(_ => ThenThePipelineIsBuilt()) + .BDDfy(); + } + + private void ThenThePipelineIsBuilt() + { + _handlers.ShouldNotBeNull(); + } + + private void WhenIBuild() + { + _handlers = _builder.BuildOcelotPipeline(new OcelotPipelineConfiguration()); + } + + private void GivenTheDepedenciesAreSetUp() + { + IConfigurationBuilder test = new ConfigurationBuilder(); + var root = test.Build(); + var services = new ServiceCollection(); + services.AddSingleton(root); + services.AddDiscoveryClient(new DiscoveryOptions {ClientType = DiscoveryClientType.EUREKA}); + services.AddOcelot(); + var provider = services.BuildServiceProvider(); + _builder = new OcelotPipelineBuilder(provider); + } + } +} diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs index 988d0b21..21649a80 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs @@ -1,4 +1,7 @@ -namespace Ocelot.UnitTests.Middleware +using System; +using System.Threading.Tasks; + +namespace Ocelot.UnitTests.Middleware { using System.Collections.Generic; using Microsoft.AspNetCore.Hosting; @@ -7,6 +10,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; + using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Middleware.Pipeline; using Shouldly; @@ -80,5 +84,59 @@ _counter.ShouldBe(1); _downstreamContext.HttpContext.Response.StatusCode.ShouldBe(404); } + + [Fact] + public void Middleware_Multi_Parameters_Invoke() + { + var provider = _services.BuildServiceProvider(); + IOcelotPipelineBuilder builder = new OcelotPipelineBuilder(provider); + builder = builder.UseMiddleware(); + var del = builder.Build(); + _downstreamContext = new DownstreamContext(new DefaultHttpContext()); + del.Invoke(_downstreamContext); + } + + private class MultiParametersInvokeMiddleware : OcelotMiddleware + { + private readonly OcelotRequestDelegate _next; + + public MultiParametersInvokeMiddleware(OcelotRequestDelegate next) + :base(new FakeLogger()) + { + _next = next; + } + + public Task Invoke(DownstreamContext context, IServiceProvider serviceProvider) + { + return Task.CompletedTask; + } + } + } + + class FakeLogger : IOcelotLogger + { + public void LogCritical(string message, Exception exception) + { + } + + public void LogDebug(string message) + { + } + + public void LogError(string message, Exception exception) + { + } + + public void LogInformation(string message) + { + } + + public void LogTrace(string message) + { + } + + public void LogWarning(string message) + { + } } } diff --git a/test/Ocelot.UnitTests/Middleware/ResponseAggregatorFactoryTests.cs b/test/Ocelot.UnitTests/Middleware/ResponseAggregatorFactoryTests.cs new file mode 100644 index 00000000..b4413b45 --- /dev/null +++ b/test/Ocelot.UnitTests/Middleware/ResponseAggregatorFactoryTests.cs @@ -0,0 +1,64 @@ +namespace Ocelot.UnitTests.Middleware +{ + using Moq; + using Ocelot.Configuration.Builder; + using Ocelot.Middleware.Multiplexer; + using Shouldly; + using Xunit; + using Ocelot.Configuration; + using TestStack.BDDfy; + + public class ResponseAggregatorFactoryTests + { + private readonly InMemoryResponseAggregatorFactory _factory; + private Mock _provider; + private ReRoute _reRoute; + private IResponseAggregator _aggregator; + + public ResponseAggregatorFactoryTests() + { + _provider = new Mock(); + _factory = new InMemoryResponseAggregatorFactory(_provider.Object); + } + + [Fact] + public void should_return_simple_json_aggregator() + { + var reRoute = new ReRouteBuilder() + .Build(); + + this.Given(_ => GivenReRoute(reRoute)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheAggregatorIs()) + .BDDfy(); + } + + [Fact] + public void should_return_user_defined_aggregator() + { + var reRoute = new ReRouteBuilder() + .WithAggregator("doesntmatter") + .Build(); + + this.Given(_ => GivenReRoute(reRoute)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheAggregatorIs()) + .BDDfy(); + } + + private void GivenReRoute(ReRoute reRoute) + { + _reRoute = reRoute; + } + + private void WhenIGet() + { + _aggregator = _factory.Get(_reRoute); + } + + private void ThenTheAggregatorIs() + { + _aggregator.ShouldBeOfType(); + } + } +} diff --git a/test/Ocelot.UnitTests/Middleware/SimpleJsonResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Middleware/SimpleJsonResponseAggregatorTests.cs index 27d1b7e0..fb2a1c98 100644 --- a/test/Ocelot.UnitTests/Middleware/SimpleJsonResponseAggregatorTests.cs +++ b/test/Ocelot.UnitTests/Middleware/SimpleJsonResponseAggregatorTests.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Text; +using Castle.Components.DictionaryAdapter; using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Errors; using Ocelot.Middleware; using Ocelot.Middleware.Multiplexer; +using Ocelot.Request.Middleware; using Ocelot.UnitTests.Responder; using Shouldly; using TestStack.BDDfy; @@ -28,40 +30,6 @@ namespace Ocelot.UnitTests.Middleware _aggregator = new SimpleJsonResponseAggregator(); } - [Fact] - public void should_map_all_downstream_to_upstream_when_not_aggregate() - { - var billDownstreamReRoute = new DownstreamReRouteBuilder().WithKey("Bill").Build(); - - var downstreamReRoutes = new List - { - billDownstreamReRoute, - }; - - var reRoute = new ReRouteBuilder() - .WithDownstreamReRoutes(downstreamReRoutes) - .Build(); - - var billDownstreamContext = new DownstreamContext(new DefaultHttpContext()) - { - DownstreamResponse = - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("Bill says hi") }, - DownstreamReRoute = billDownstreamReRoute, - Errors = new List { new AnyError() }, - DownstreamRequest = new HttpRequestMessage(HttpMethod.Get, new Uri("http://www.bbc.co.uk")), - }; - - var downstreamContexts = new List { billDownstreamContext }; - - this.Given(x => GivenTheUpstreamContext(new DownstreamContext(new DefaultHttpContext()))) - .And(x => GivenTheReRoute(reRoute)) - .And(x => GivenTheDownstreamContext(downstreamContexts)) - .When(x => WhenIAggregate()) - .Then(x => ThenTheContentIs("Bill says hi")) - .And(x => ThenTheUpstreamContextIsMappedForNonAggregate()) - .BDDfy(); - } - [Fact] public void should_aggregate_n_responses_and_set_response_content_on_upstream_context() { @@ -81,15 +49,13 @@ namespace Ocelot.UnitTests.Middleware var billDownstreamContext = new DownstreamContext(new DefaultHttpContext()) { - DownstreamResponse = - new HttpResponseMessage(HttpStatusCode.OK) {Content = new StringContent("Bill says hi")}, + DownstreamResponse = new DownstreamResponse(new StringContent("Bill says hi"), HttpStatusCode.OK, new EditableList>>()), DownstreamReRoute = billDownstreamReRoute }; var georgeDownstreamContext = new DownstreamContext(new DefaultHttpContext()) { - DownstreamResponse = - new HttpResponseMessage(HttpStatusCode.OK) {Content = new StringContent("George says hi")}, + DownstreamResponse = new DownstreamResponse(new StringContent("George says hi"), HttpStatusCode.OK, new List>>()), DownstreamReRoute = georgeDownstreamReRoute }; @@ -125,19 +91,18 @@ namespace Ocelot.UnitTests.Middleware var billDownstreamContext = new DownstreamContext(new DefaultHttpContext()) { - DownstreamResponse = - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("Bill says hi") }, + DownstreamResponse = new DownstreamResponse(new StringContent("Bill says hi"), HttpStatusCode.OK, new List>>()), DownstreamReRoute = billDownstreamReRoute }; var georgeDownstreamContext = new DownstreamContext(new DefaultHttpContext()) { - DownstreamResponse = - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("Error") }, + DownstreamResponse = new DownstreamResponse(new StringContent("Error"), HttpStatusCode.OK, new List>>()), DownstreamReRoute = georgeDownstreamReRoute, - Errors = new List() { new AnyError() } }; + georgeDownstreamContext.Errors.Add(new AnyError()); + var downstreamContexts = new List { billDownstreamContext, georgeDownstreamContext }; var expected = "Error"; diff --git a/test/Ocelot.UnitTests/Middleware/UserDefinedResponseAggregatorTests.cs b/test/Ocelot.UnitTests/Middleware/UserDefinedResponseAggregatorTests.cs new file mode 100644 index 00000000..2b5bd552 --- /dev/null +++ b/test/Ocelot.UnitTests/Middleware/UserDefinedResponseAggregatorTests.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Moq; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; +using Ocelot.Responses; +using Ocelot.UnitTests.Responder; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Middleware +{ + public class UserDefinedResponseAggregatorTests + { + private readonly UserDefinedResponseAggregator _aggregator; + private readonly Mock _provider; + private ReRoute _reRoute; + private List _contexts; + private DownstreamContext _context; + + public UserDefinedResponseAggregatorTests() + { + _provider = new Mock(); + _aggregator = new UserDefinedResponseAggregator(_provider.Object); + } + + [Fact] + public void should_call_aggregator() + { + var reRoute = new ReRouteBuilder().Build(); + + var context = new DownstreamContext(new DefaultHttpContext()); + + var contexts = new List + { + new DownstreamContext(new DefaultHttpContext()) + { + DownstreamResponse = new DownstreamResponse(new StringContent("Tom"), HttpStatusCode.OK, new List>>()) + }, + new DownstreamContext(new DefaultHttpContext()) + { + DownstreamResponse = new DownstreamResponse(new StringContent("Laura"), HttpStatusCode.OK, new List>>()) + } + }; + + this.Given(_ => GivenTheProviderReturnsAggregator()) + .And(_ => GivenReRoute(reRoute)) + .And(_ => GivenContexts(contexts)) + .And(_ => GivenContext(context)) + .When(_ => WhenIAggregate()) + .Then(_ => ThenTheProviderIsCalled()) + .And(_ => ThenTheContentIsCorrect()) + .BDDfy(); + } + + [Fact] + public void should_not_find_aggregator() + { + var reRoute = new ReRouteBuilder().Build(); + + var context = new DownstreamContext(new DefaultHttpContext()); + + var contexts = new List + { + new DownstreamContext(new DefaultHttpContext()) + { + DownstreamResponse = new DownstreamResponse(new StringContent("Tom"), HttpStatusCode.OK, new List>>()) + }, + new DownstreamContext(new DefaultHttpContext()) + { + DownstreamResponse = new DownstreamResponse(new StringContent("Laura"), HttpStatusCode.OK, new List>>()) + } + }; + + this.Given(_ => GivenTheProviderReturnsError()) + .And(_ => GivenReRoute(reRoute)) + .And(_ => GivenContexts(contexts)) + .And(_ => GivenContext(context)) + .When(_ => WhenIAggregate()) + .Then(_ => ThenTheProviderIsCalled()) + .And(_ => ThenTheErrorIsReturned()) + .BDDfy(); + } + + private void ThenTheErrorIsReturned() + { + _context.IsError.ShouldBeTrue(); + _context.Errors.Count.ShouldBe(1); + } + + private void GivenTheProviderReturnsError() + { + _provider.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); + } + + private async Task ThenTheContentIsCorrect() + { + var content = await _context.DownstreamResponse.Content.ReadAsStringAsync(); + content.ShouldBe("Tom, Laura"); + } + + private void ThenTheProviderIsCalled() + { + _provider.Verify(x => x.Get(_reRoute), Times.Once); + } + + private void GivenContext(DownstreamContext context) + { + _context = context; + } + + private void GivenContexts(List contexts) + { + _contexts = contexts; + } + + private async Task WhenIAggregate() + { + await _aggregator.Aggregate(_reRoute, _context, _contexts); + } + + private void GivenTheProviderReturnsAggregator() + { + var aggregator = new TestDefinedAggregator(); + _provider.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(aggregator)); + } + + private void GivenReRoute(ReRoute reRoute) + { + _reRoute = reRoute; + } + + public class TestDefinedAggregator : IDefinedAggregator + { + public async Task Aggregate(List responses) + { + var tom = await responses[0].Content.ReadAsStringAsync(); + var laura = await responses[1].Content.ReadAsStringAsync(); + var content = $"{tom}, {laura}"; + var headers = responses.SelectMany(x => x.Headers).ToList(); + return new DownstreamResponse(new StringContent(content), HttpStatusCode.OK, headers); + } + } + } +} diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index 80084a4d..395dde67 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -28,33 +28,31 @@ - - - PreserveNewest - - - - - - + + + all - - - - - - - - - + + + + + + + + + + + + + diff --git a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs index d12d4294..8672ebf5 100644 --- a/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs @@ -12,24 +12,27 @@ using TestStack.BDDfy; using Xunit; using System.Net.Http; using System; +using Ocelot.Request.Middleware; namespace Ocelot.UnitTests.QueryStrings { public class AddQueriesToRequestTests { private readonly AddQueriesToRequest _addQueriesToRequest; - private readonly HttpRequestMessage _downstreamRequest; + private DownstreamRequest _downstreamRequest; private readonly Mock _parser; private List _configuration; private List _claims; private Response _result; private Response _claimValue; + private HttpRequestMessage _request; public AddQueriesToRequestTests() { + _request = new HttpRequestMessage(HttpMethod.Post, "http://my.url/abc?q=123"); _parser = new Mock(); _addQueriesToRequest = new AddQueriesToRequest(_parser.Object); - _downstreamRequest = new HttpRequestMessage(HttpMethod.Post, "http://my.url/abc?q=123"); + _downstreamRequest = new DownstreamRequest(_request); } [Fact] @@ -53,6 +56,34 @@ namespace Ocelot.UnitTests.QueryStrings .BDDfy(); } + [Fact] + public void should_add_new_queries_to_downstream_request_and_preserve_other_queries() + { + var claims = new List + { + new Claim("test", "data") + }; + + this.Given( + x => x.GivenAClaimToThing(new List + { + new ClaimToThing("query-key", "", "", 0) + })) + .Given(x => x.GivenClaims(claims)) + .And(x => GivenTheDownstreamRequestHasQueryString("?test=1&test=2")) + .And(x => x.GivenTheClaimParserReturns(new OkResponse("value"))) + .When(x => x.WhenIAddQueriesToTheRequest()) + .Then(x => x.ThenTheResultIsSuccess()) + .And(x => x.ThenTheQueryIsAdded()) + .And(x => TheTheQueryStringIs("?test=1&test=2&query-key=value")) + .BDDfy(); + } + + private void TheTheQueryStringIs(string expected) + { + _downstreamRequest.Query.ShouldBe(expected); + } + [Fact] public void should_replace_existing_queries_on_downstream_request() { @@ -95,7 +126,7 @@ namespace Ocelot.UnitTests.QueryStrings private void ThenTheQueryIsAdded() { - var queries = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_downstreamRequest.RequestUri.OriginalString); + var queries = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_downstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString); var query = queries.First(x => x.Key == "query-key"); query.Value.First().ShouldBe(_claimValue.Data); } @@ -110,12 +141,18 @@ namespace Ocelot.UnitTests.QueryStrings _claims = claims; } + private void GivenTheDownstreamRequestHasQueryString(string queryString) + { + _request = new HttpRequestMessage(HttpMethod.Post, $"http://my.url/abc{queryString}"); + _downstreamRequest = new DownstreamRequest(_request); + } + private void GivenTheDownstreamRequestHasQueryString(string key, string value) { var newUri = Microsoft.AspNetCore.WebUtilities.QueryHelpers - .AddQueryString(_downstreamRequest.RequestUri.OriginalString, key, value); + .AddQueryString(_downstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString, key, value); - _downstreamRequest.RequestUri = new Uri(newUri); + _request.RequestUri = new Uri(newUri); } private void GivenTheClaimParserReturns(Response claimValue) diff --git a/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs b/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs index 163a0411..e4d95028 100644 --- a/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/QueryStrings/QueryStringBuilderMiddlewareTests.cs @@ -18,6 +18,7 @@ namespace Ocelot.UnitTests.QueryStrings using System.Security.Claims; using Microsoft.AspNetCore.Http; using System.Threading.Tasks; + using Ocelot.Request.Middleware; public class QueryStringBuilderMiddlewareTests { @@ -36,7 +37,7 @@ namespace Ocelot.UnitTests.QueryStrings _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _addQueries = new Mock(); - _downstreamContext.DownstreamRequest = new HttpRequestMessage(); + _downstreamContext.DownstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); _middleware = new QueryStringBuilderMiddleware(_next, _loggerFactory.Object, _addQueries.Object); } @@ -74,7 +75,7 @@ namespace Ocelot.UnitTests.QueryStrings .Setup(x => x.SetQueriesOnDownstreamRequest( It.IsAny>(), It.IsAny>(), - It.IsAny())) + It.IsAny())) .Returns(new OkResponse()); } diff --git a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs index 819e38dd..7d3cac1a 100644 --- a/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RateLimit/ClientRateLimitMiddlewareTests.cs @@ -18,6 +18,7 @@ namespace Ocelot.UnitTests.RateLimit using Microsoft.Extensions.Caching.Memory; using System.IO; using System.Threading.Tasks; + using Ocelot.Request.Middleware; public class ClientRateLimitMiddlewareTests { @@ -100,7 +101,7 @@ namespace Ocelot.UnitTests.RateLimit { var request = new HttpRequestMessage(new HttpMethod("GET"), _url); request.Headers.Add("ClientId", clientId); - _downstreamContext.DownstreamRequest = request; + _downstreamContext.DownstreamRequest = new DownstreamRequest(request); _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); _responseStatusCode = (int)_downstreamContext.HttpContext.Response.StatusCode; @@ -115,7 +116,7 @@ namespace Ocelot.UnitTests.RateLimit { var request = new HttpRequestMessage(new HttpMethod("GET"), _url); request.Headers.Add("ClientId", clientId); - _downstreamContext.DownstreamRequest = request; + _downstreamContext.DownstreamRequest = new DownstreamRequest(request); _downstreamContext.HttpContext.Request.Headers.TryAdd("ClientId", clientId); _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); diff --git a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs index 2bd26a4b..d37efc42 100644 --- a/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs @@ -88,7 +88,7 @@ namespace Ocelot.UnitTests.Request private void GivenTheMapperWillReturnAMappedRequest() { - _mappedRequest = new OkResponse(new HttpRequestMessage()); + _mappedRequest = new OkResponse(new HttpRequestMessage(HttpMethod.Get, "http://www.bbc.co.uk")); _requestMapper .Setup(rm => rm.Map(It.IsAny())) diff --git a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs index ba719a45..22ff9276 100644 --- a/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs +++ b/test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs @@ -163,7 +163,6 @@ .BDDfy(); } - [Fact] public void should_not_add_content_headers() { diff --git a/test/Ocelot.UnitTests/RequestId/ReRouteRequestIdMiddlewareTests.cs b/test/Ocelot.UnitTests/RequestId/ReRouteRequestIdMiddlewareTests.cs index e27fc50f..44d9c405 100644 --- a/test/Ocelot.UnitTests/RequestId/ReRouteRequestIdMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/RequestId/ReRouteRequestIdMiddlewareTests.cs @@ -1,9 +1,6 @@ -using Ocelot.Middleware; - -namespace Ocelot.UnitTests.RequestId +namespace Ocelot.UnitTests.RequestId { using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Primitives; using Ocelot.Infrastructure.RequestData; using System; using System.Collections.Generic; @@ -20,6 +17,8 @@ namespace Ocelot.UnitTests.RequestId using Shouldly; using TestStack.BDDfy; using Xunit; + using Ocelot.Request.Middleware; + using Ocelot.Middleware; public class ReRouteRequestIdMiddlewareTests { @@ -35,7 +34,7 @@ namespace Ocelot.UnitTests.RequestId public ReRouteRequestIdMiddlewareTests() { - _downstreamRequest = new HttpRequestMessage(); + _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, "http://test.com"); _repo = new Mock(); _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); @@ -47,7 +46,7 @@ namespace Ocelot.UnitTests.RequestId return Task.CompletedTask; }; _middleware = new ReRouteRequestIdMiddleware(_next, _loggerFactory.Object, _repo.Object); - _downstreamContext.DownstreamRequest = _downstreamRequest; + _downstreamContext.DownstreamRequest = new DownstreamRequest(_downstreamRequest); } [Fact] @@ -141,6 +140,30 @@ namespace Ocelot.UnitTests.RequestId .BDDfy(); } + [Fact] + public void should_not_update_if_global_request_id_is_same_as_re_route_request_id() + { + var downstreamRoute = new DownstreamRoute(new List(), + new ReRouteBuilder() + .WithDownstreamReRoute(new DownstreamReRouteBuilder() + .WithDownstreamPathTemplate("any old string") + .WithRequestIdKey("LSRequestId") + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()) + .WithUpstreamHttpMethod(new List { "Get" }) + .Build()); + + var requestId = "alreadyset"; + + this.Given(x => x.GivenTheDownStreamRouteIs(downstreamRoute)) + .And(x => GivenTheRequestIdWasSetGlobally()) + .And(x => x.GivenTheRequestIdIsAddedToTheRequest("LSRequestId", requestId)) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheTraceIdIs(requestId)) + .And(x => ThenTheRequestIdIsNotUpdated()) + .BDDfy(); + } + private void WhenICallTheMiddleware() { _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); @@ -158,12 +181,17 @@ namespace Ocelot.UnitTests.RequestId private void ThenTheRequestIdIsSaved() { - _repo.Verify(x => x.Add("RequestId", _value), Times.Once); + _repo.Verify(x => x.Add("RequestId", _value), Times.Once); } private void ThenTheRequestIdIsUpdated() { - _repo.Verify(x => x.Update("RequestId", _value), Times.Once); + _repo.Verify(x => x.Update("RequestId", _value), Times.Once); + } + + private void ThenTheRequestIdIsNotUpdated() + { + _repo.Verify(x => x.Update("RequestId", _value), Times.Never); } private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) @@ -181,15 +209,13 @@ namespace Ocelot.UnitTests.RequestId private void ThenTheTraceIdIsAnything() { - StringValues value; - _downstreamContext.HttpContext.Response.Headers.TryGetValue("LSRequestId", out value); + _downstreamContext.HttpContext.Response.Headers.TryGetValue("LSRequestId", out var value); value.First().ShouldNotBeNullOrEmpty(); } private void ThenTheTraceIdIs(string expected) { - StringValues value; - _downstreamContext.HttpContext.Response.Headers.TryGetValue("LSRequestId", out value); + _downstreamContext.HttpContext.Response.Headers.TryGetValue("LSRequestId", out var value); value.First().ShouldBe(expected); } } diff --git a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs index c4ab1341..d4bc994c 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs @@ -1,9 +1,20 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Middleware; +using Ocelot.Request.Middleware; using Ocelot.Requester; using Ocelot.Responses; using Shouldly; @@ -12,33 +23,72 @@ using Xunit; namespace Ocelot.UnitTests.Requester { - public class HttpClientBuilderTests + public class HttpClientBuilderTests : IDisposable { private readonly HttpClientBuilder _builder; private readonly Mock _factory; private IHttpClient _httpClient; private HttpResponseMessage _response; - private DownstreamReRoute _request; + private DownstreamContext _context; + private readonly Mock _cacheHandlers; + private Mock _logger; + private int _count; + private IWebHost _host; public HttpClientBuilderTests() { + _cacheHandlers = new Mock(); + _logger = new Mock(); _factory = new Mock(); - _builder = new HttpClientBuilder(_factory.Object); + _builder = new HttpClientBuilder(_factory.Object, _cacheHandlers.Object, _logger.Object); } [Fact] public void should_build_http_client() { + var reRoute = new DownstreamReRouteBuilder() + .WithIsQos(false) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithReRouteKey("") + .WithQosOptions(new QoSOptionsBuilder().Build()) + .Build(); + this.Given(x => GivenTheFactoryReturns()) - .And(x => GivenARequest()) + .And(x => GivenARequest(reRoute)) .When(x => WhenIBuild()) .Then(x => ThenTheHttpClientShouldNotBeNull()) .BDDfy(); } + [Fact] + public void should_log_if_ignoring_ssl_errors() + { + var reRoute = new DownstreamReRouteBuilder() + .WithIsQos(false) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithReRouteKey("") + .WithQosOptions(new QoSOptionsBuilder().Build()) + .WithDangerousAcceptAnyServerCertificateValidator(true) + .Build(); + + this.Given(x => GivenTheFactoryReturns()) + .And(x => GivenARequest(reRoute)) + .When(x => WhenIBuild()) + .Then(x => ThenTheHttpClientShouldNotBeNull()) + .Then(x => ThenTheDangerousAcceptAnyServerCertificateValidatorWarningIsLogged()) + .BDDfy(); + } + [Fact] public void should_call_delegating_handlers_in_order() { + var reRoute = new DownstreamReRouteBuilder() + .WithIsQos(false) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithReRouteKey("") + .WithQosOptions(new QoSOptionsBuilder().Build()) + .Build(); + var fakeOne = new FakeDelegatingHandler(); var fakeTwo = new FakeDelegatingHandler(); @@ -49,7 +99,7 @@ namespace Ocelot.UnitTests.Requester }; this.Given(x => GivenTheFactoryReturns(handlers)) - .And(x => GivenARequest()) + .And(x => GivenARequest(reRoute)) .And(x => WhenIBuild()) .When(x => WhenICallTheClient()) .Then(x => ThenTheFakeAreHandledInOrder(fakeOne, fakeTwo)) @@ -57,12 +107,104 @@ namespace Ocelot.UnitTests.Requester .BDDfy(); } - private void GivenARequest() + [Fact] + public void should_re_use_cookies_from_container() { - var reRoute = new DownstreamReRouteBuilder().WithIsQos(false) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)).WithReRouteKey("").Build(); + var reRoute = new DownstreamReRouteBuilder() + .WithIsQos(false) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, true, false)) + .WithReRouteKey("") + .WithQosOptions(new QoSOptionsBuilder().Build()) + .Build(); - _request = reRoute; + this.Given(_ => GivenADownstreamService()) + .And(_ => GivenARequest(reRoute)) + .And(_ => GivenTheFactoryReturnsNothing()) + .And(_ => WhenIBuild()) + .And(_ => WhenICallTheClient("http://localhost:5003")) + .And(_ => ThenTheCookieIsSet()) + .And(_ => GivenTheClientIsCached()) + .And(_ => WhenIBuild()) + .When(_ => WhenICallTheClient("http://localhost:5003")) + .Then(_ => ThenTheResponseIsOk()) + .BDDfy(); + } + + private void ThenTheDangerousAcceptAnyServerCertificateValidatorWarningIsLogged() + { + _logger.Verify(x => x.LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamReRoute, UpstreamPathTemplate: {_context.DownstreamReRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {_context.DownstreamReRoute.DownstreamPathTemplate}"), Times.Once); + } + + private void GivenTheClientIsCached() + { + _cacheHandlers.Setup(x => x.Get(It.IsAny())).Returns(_httpClient); + } + + private void ThenTheCookieIsSet() + { + _response.Headers.TryGetValues("Set-Cookie", out var test).ShouldBeTrue(); + } + + private void WhenICallTheClient(string url) + { + _response = _httpClient + .SendAsync(new HttpRequestMessage(HttpMethod.Get, url)) + .GetAwaiter() + .GetResult(); + } + + private void ThenTheResponseIsOk() + { + _response.StatusCode.ShouldBe(HttpStatusCode.OK); + } + + private void GivenADownstreamService() + { + _host = new WebHostBuilder() + .UseUrls("http://localhost:5003") + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.Run(context => + { + if (_count == 0) + { + context.Response.Cookies.Append("test", "0"); + context.Response.StatusCode = 200; + _count++; + return Task.CompletedTask; + } + + if (_count == 1) + { + if (context.Request.Cookies.TryGetValue("test", out var cookieValue) || context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) + { + context.Response.StatusCode = 200; + return Task.CompletedTask; + } + + context.Response.StatusCode = 500; + } + + return Task.CompletedTask; + }); + }) + .Build(); + + _host.Start(); + } + + private void GivenARequest(DownstreamReRoute downstream) + { + var context = new DownstreamContext(new DefaultHttpContext()) + { + DownstreamReRoute = downstream, + DownstreamRequest = new DownstreamRequest(new HttpRequestMessage() { RequestUri = new Uri("http://localhost:5003") }), + }; + + _context = context; } private void ThenSomethingIsReturned() @@ -89,6 +231,15 @@ namespace Ocelot.UnitTests.Requester .Returns(new OkResponse>>(handlers)); } + private void GivenTheFactoryReturnsNothing() + { + var handlers = new List>(); + + _factory + .Setup(x => x.Get(It.IsAny())) + .Returns(new OkResponse>>(handlers)); + } + private void GivenTheFactoryReturns(List> handlers) { _factory @@ -98,12 +249,18 @@ namespace Ocelot.UnitTests.Requester private void WhenIBuild() { - _httpClient = _builder.Create(_request); + _httpClient = _builder.Create(_context); } private void ThenTheHttpClientShouldNotBeNull() { _httpClient.ShouldNotBeNull(); } + + public void Dispose() + { + _response?.Dispose(); + _host?.Dispose(); + } } } diff --git a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs index f2c1f3a0..d2210cf0 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientHttpRequesterTest.cs @@ -12,13 +12,16 @@ using Ocelot.Middleware; using TestStack.BDDfy; using Xunit; using Shouldly; +using Ocelot.Request.Middleware; +using System.Threading.Tasks; +using System.Threading; namespace Ocelot.UnitTests.Requester { public class HttpClientHttpRequesterTest { private readonly Mock _cacheHandlers; - private Mock _house; + private readonly Mock _factory; private Response _response; private readonly HttpClientHttpRequester _httpClientRequester; private DownstreamContext _request; @@ -27,30 +30,38 @@ namespace Ocelot.UnitTests.Requester public HttpClientHttpRequesterTest() { - _house = new Mock(); - _house.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse>>(new List>())); + _factory = new Mock(); + _factory.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse>>(new List>())); _logger = new Mock(); _loggerFactory = new Mock(); _loggerFactory .Setup(x => x.CreateLogger()) .Returns(_logger.Object); _cacheHandlers = new Mock(); - _httpClientRequester = new HttpClientHttpRequester(_loggerFactory.Object, _cacheHandlers.Object, _house.Object); + _httpClientRequester = new HttpClientHttpRequester( + _loggerFactory.Object, + _cacheHandlers.Object, + _factory.Object); } [Fact] public void should_call_request_correctly() { - var reRoute = new DownstreamReRouteBuilder().WithIsQos(false) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)).WithReRouteKey("").Build(); + var reRoute = new DownstreamReRouteBuilder() + .WithIsQos(false) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithReRouteKey("") + .WithQosOptions(new QoSOptionsBuilder().Build()) + .Build(); var context = new DownstreamContext(new DefaultHttpContext()) { DownstreamReRoute = reRoute, - DownstreamRequest = new HttpRequestMessage() { RequestUri = new Uri("http://www.bbc.co.uk") }, + DownstreamRequest = new DownstreamRequest(new HttpRequestMessage() { RequestUri = new Uri("http://www.bbc.co.uk") }), }; this.Given(x=>x.GivenTheRequestIs(context)) + .And(x => GivenTheHouseReturnsOkHandler()) .When(x=>x.WhenIGetResponse()) .Then(x => x.ThenTheResponseIsCalledCorrectly()) .BDDfy(); @@ -59,13 +70,17 @@ namespace Ocelot.UnitTests.Requester [Fact] public void should_call_request_unable_to_complete_request() { - var reRoute = new DownstreamReRouteBuilder().WithIsQos(false) - .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)).WithReRouteKey("").Build(); + var reRoute = new DownstreamReRouteBuilder() + .WithIsQos(false) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithReRouteKey("") + .WithQosOptions(new QoSOptionsBuilder().Build()) + .Build(); var context = new DownstreamContext(new DefaultHttpContext()) { DownstreamReRoute = reRoute, - DownstreamRequest = new HttpRequestMessage() { RequestUri = new Uri("http://localhost:60080") }, + DownstreamRequest = new DownstreamRequest(new HttpRequestMessage() { RequestUri = new Uri("http://localhost:60080") }), }; this.Given(x => x.GivenTheRequestIs(context)) @@ -74,6 +89,30 @@ namespace Ocelot.UnitTests.Requester .BDDfy(); } + [Fact] + public void http_client_request_times_out() + { + var reRoute = new DownstreamReRouteBuilder() + .WithIsQos(false) + .WithHttpHandlerOptions(new HttpHandlerOptions(false, false, false)) + .WithReRouteKey("") + .WithQosOptions(new QoSOptionsBuilder().WithTimeoutValue(1).Build()) + .Build(); + + var context = new DownstreamContext(new DefaultHttpContext()) + { + DownstreamReRoute = reRoute, + DownstreamRequest = new DownstreamRequest(new HttpRequestMessage() { RequestUri = new Uri("http://localhost:60080") }), + }; + + this.Given(_ => GivenTheRequestIs(context)) + .And(_ => GivenTheHouseReturnsTimeoutHandler()) + .When(_ => WhenIGetResponse()) + .Then(_ => ThenTheResponseIsCalledError()) + .And(_ => ThenTheErrorIsTimeout()) + .BDDfy(); + } + private void GivenTheRequestIs(DownstreamContext request) { _request = request; @@ -93,5 +132,47 @@ namespace Ocelot.UnitTests.Requester { _response.IsError.ShouldBeTrue(); } + + private void ThenTheErrorIsTimeout() + { + _response.Errors[0].ShouldBeOfType(); + } + + private void GivenTheHouseReturnsOkHandler() + { + var handlers = new List> + { + () => new OkDelegatingHandler() + }; + + _factory.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse>>(handlers)); + } + + private void GivenTheHouseReturnsTimeoutHandler() + { + var handlers = new List> + { + () => new TimeoutDelegatingHandler() + }; + + _factory.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse>>(handlers)); + } + + class OkDelegatingHandler : DelegatingHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage()); + } + } + + class TimeoutDelegatingHandler : DelegatingHandler + { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + await Task.Delay(100000, cancellationToken); + return new HttpResponseMessage(); + } + } } } diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index f4facf5e..910b3be8 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -1,6 +1,3 @@ -using Ocelot.Configuration.Builder; -using Ocelot.Middleware; - namespace Ocelot.UnitTests.Requester { using Microsoft.AspNetCore.Http; @@ -14,11 +11,16 @@ namespace Ocelot.UnitTests.Requester using Xunit; using Shouldly; using System.Threading.Tasks; + using Ocelot.Configuration.Builder; + using Ocelot.Middleware; + using System; + using System.Linq; + using Ocelot.UnitTests.Responder; public class HttpRequesterMiddlewareTests { private readonly Mock _requester; - private OkResponse _response; + private Response _response; private Mock _loggerFactory; private Mock _logger; private readonly HttpRequesterMiddleware _middleware; @@ -39,12 +41,27 @@ namespace Ocelot.UnitTests.Requester public void should_call_services_correctly() { this.Given(x => x.GivenTheRequestIs()) - .And(x => x.GivenTheRequesterReturns(new HttpResponseMessage())) + .And(x => x.GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage()))) .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheScopedRepoIsCalledCorrectly()) + .Then(x => x.ThenTheDownstreamResponseIsSet()) .BDDfy(); } + [Fact] + public void should_set_error() + { + this.Given(x => x.GivenTheRequestIs()) + .And(x => x.GivenTheRequesterReturns(new ErrorResponse(new AnyError()))) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheErrorIsSet()) + .BDDfy(); + } + + private void ThenTheErrorIsSet() + { + _downstreamContext.IsError.ShouldBeTrue(); + } + private void WhenICallTheMiddleware() { _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); @@ -52,21 +69,34 @@ namespace Ocelot.UnitTests.Requester private void GivenTheRequestIs() { - _downstreamContext = new DownstreamContext(new DefaultHttpContext()); - _downstreamContext.DownstreamReRoute = new DownstreamReRouteBuilder().Build(); + _downstreamContext = + new DownstreamContext(new DefaultHttpContext()) + { + DownstreamReRoute = new DownstreamReRouteBuilder().Build() + }; } - private void GivenTheRequesterReturns(HttpResponseMessage response) + private void GivenTheRequesterReturns(Response response) { - _response = new OkResponse(response); + _response = response; + _requester .Setup(x => x.GetResponse(It.IsAny())) .ReturnsAsync(_response); } - private void ThenTheScopedRepoIsCalledCorrectly() + private void ThenTheDownstreamResponseIsSet() { - _downstreamContext.DownstreamResponse.ShouldBe(_response.Data); + foreach (var httpResponseHeader in _response.Data.Headers) + { + if (_downstreamContext.DownstreamResponse.Headers.Any(x => x.Key == httpResponseHeader.Key)) + { + throw new Exception("Header in response not in downstreamresponse headers"); + } + } + + _downstreamContext.DownstreamResponse.Content.ShouldBe(_response.Data.Content); + _downstreamContext.DownstreamResponse.StatusCode.ShouldBe(_response.Data.StatusCode); } } } diff --git a/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs b/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs index e8196966..bd633cb3 100644 --- a/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/TracingHandlerFactoryTests.cs @@ -1,5 +1,6 @@ using Butterfly.Client.Tracing; using Moq; +using Ocelot.Infrastructure.RequestData; using Ocelot.Requester; using Shouldly; using Xunit; @@ -10,11 +11,13 @@ namespace Ocelot.UnitTests.Requester { private TracingHandlerFactory _factory; private Mock _tracer; + private Mock _repo; public TracingHandlerFactoryTests() { _tracer = new Mock(); - _factory = new TracingHandlerFactory(_tracer.Object); + _repo = new Mock(); + _factory = new TracingHandlerFactory(_tracer.Object, _repo.Object); } [Fact] diff --git a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs index 9800ffef..27417907 100644 --- a/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs +++ b/test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs @@ -120,7 +120,7 @@ namespace Ocelot.UnitTests.Responder // If this test fails then it's because the number of error codes has changed. // You should make the appropriate changes to the test cases here to ensure // they cover all the error codes, and then modify this assertion. - Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(33, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); + Enum.GetNames(typeof(OcelotErrorCode)).Length.ShouldBe(35, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); } private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode) diff --git a/test/Ocelot.UnitTests/Responder/HttpContextResponderTests.cs b/test/Ocelot.UnitTests/Responder/HttpContextResponderTests.cs index ae465774..6ff956d2 100644 --- a/test/Ocelot.UnitTests/Responder/HttpContextResponderTests.cs +++ b/test/Ocelot.UnitTests/Responder/HttpContextResponderTests.cs @@ -1,10 +1,11 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using System.Net; using System.Net.Http; -using System.Text; using Microsoft.AspNetCore.Http; using Ocelot.Headers; +using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; using Ocelot.Responder; using Shouldly; using Xunit; @@ -26,9 +27,13 @@ namespace Ocelot.UnitTests.Responder public void should_remove_transfer_encoding_header() { var httpContext = new DefaultHttpContext(); - var httpResponseMessage = new HttpResponseMessage {Content = new StringContent("")}; - httpResponseMessage.Headers.Add("Transfer-Encoding", "woop"); - _responder.SetResponseOnHttpContext(httpContext, httpResponseMessage).GetAwaiter().GetResult(); + var response = new DownstreamResponse(new StringContent(""), HttpStatusCode.OK, + new List>> + { + new KeyValuePair>("Transfer-Encoding", new List {"woop"}) + }); + + _responder.SetResponseOnHttpContext(httpContext, response).GetAwaiter().GetResult(); var header = httpContext.Response.Headers["Transfer-Encoding"]; header.ShouldBeEmpty(); } @@ -37,8 +42,10 @@ namespace Ocelot.UnitTests.Responder public void should_have_content_length() { var httpContext = new DefaultHttpContext(); - var httpResponseMessage = new HttpResponseMessage { Content = new StringContent("test") }; - _responder.SetResponseOnHttpContext(httpContext, httpResponseMessage).GetAwaiter().GetResult(); + var response = new DownstreamResponse(new StringContent("test"), HttpStatusCode.OK, + new List>>()); + + _responder.SetResponseOnHttpContext(httpContext, response).GetAwaiter().GetResult(); var header = httpContext.Response.Headers["Content-Length"]; header.First().ShouldBe("4"); } @@ -47,9 +54,13 @@ namespace Ocelot.UnitTests.Responder public void should_add_header() { var httpContext = new DefaultHttpContext(); - var httpResponseMessage = new HttpResponseMessage { Content = new StringContent("test") }; - httpResponseMessage.Headers.Add("test", "test"); - _responder.SetResponseOnHttpContext(httpContext, httpResponseMessage).GetAwaiter().GetResult(); + var response = new DownstreamResponse(new StringContent(""), HttpStatusCode.OK, + new List>> + { + new KeyValuePair>("test", new List {"test"}) + }); + + _responder.SetResponseOnHttpContext(httpContext, response).GetAwaiter().GetResult(); var header = httpContext.Response.Headers["test"]; header.First().ShouldBe("test"); } diff --git a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs index 2e9ffcd6..79262997 100644 --- a/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Ocelot.Middleware; +using Ocelot.Middleware.Multiplexer; namespace Ocelot.UnitTests.Responder { @@ -40,8 +41,7 @@ namespace Ocelot.UnitTests.Responder [Fact] public void should_not_return_any_errors() { - this.Given(x => x.GivenTheHttpResponseMessageIs(new HttpResponseMessage())) - .And(x => x.GivenThereAreNoPipelineErrors()) + this.Given(x => x.GivenTheHttpResponseMessageIs(new DownstreamResponse(new HttpResponseMessage()))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenThereAreNoErrors()) .BDDfy(); @@ -50,8 +50,8 @@ namespace Ocelot.UnitTests.Responder [Fact] public void should_return_any_errors() { - this.Given(x => x.GivenTheHttpResponseMessageIs(new HttpResponseMessage())) - .And(x => x.GivenThereArePipelineErrors(new UnableToFindDownstreamRouteError())) + this.Given(x => x.GivenTheHttpResponseMessageIs(new DownstreamResponse(new HttpResponseMessage()))) + .And(x => x.GivenThereArePipelineErrors(new UnableToFindDownstreamRouteError("/path", "GET"))) .When(x => x.WhenICallTheMiddleware()) .Then(x => x.ThenThereAreNoErrors()) .BDDfy(); @@ -62,16 +62,11 @@ namespace Ocelot.UnitTests.Responder _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); } - private void GivenTheHttpResponseMessageIs(HttpResponseMessage response) + private void GivenTheHttpResponseMessageIs(DownstreamResponse response) { _downstreamContext.DownstreamResponse = response; } - private void GivenThereAreNoPipelineErrors() - { - _downstreamContext.Errors = new List(); - } - private void ThenThereAreNoErrors() { //todo a better assert? @@ -79,7 +74,7 @@ namespace Ocelot.UnitTests.Responder private void GivenThereArePipelineErrors(Error error) { - _downstreamContext.Errors = new List(){error}; + _downstreamContext.Errors.Add(error); } } } diff --git a/test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs b/test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs deleted file mode 100644 index 15a2d673..00000000 --- a/test/Ocelot.UnitTests/ServerHostedMiddlewareTest.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace Ocelot.UnitTests -{ - using System; - using System.IO; - using System.Net.Http; - using Microsoft.AspNetCore.TestHost; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.AspNetCore.Builder; - using Moq; - using Ocelot.Infrastructure.RequestData; - - public abstract class ServerHostedMiddlewareTest : IDisposable - { - protected TestServer Server { get; private set; } - protected HttpClient Client { get; private set; } - protected string Url { get; private set; } - protected HttpResponseMessage ResponseMessage { get; private set; } - protected Mock ScopedRepository { get; private set; } - - public ServerHostedMiddlewareTest() - { - Url = "http://localhost:51879"; - ScopedRepository = new Mock(); - } - - protected virtual void GivenTheTestServerIsConfigured() - { - var builder = new WebHostBuilder() - .ConfigureServices(x => GivenTheTestServerServicesAreConfigured(x)) - .UseUrls(Url) - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .Configure(app => GivenTheTestServerPipelineIsConfigured(app)); - - Server = new TestServer(builder); - Client = Server.CreateClient(); - } - - protected virtual void GivenTheTestServerServicesAreConfigured(IServiceCollection services) - { - // override this in your test fixture to set up service dependencies - } - - protected virtual void GivenTheTestServerPipelineIsConfigured(IApplicationBuilder app) - { - // override this in your test fixture to set up the test server pipeline - } - - protected void WhenICallTheMiddleware() - { - ResponseMessage = Client.GetAsync(Url).Result; - } - - protected void WhenICallTheMiddlewareWithTheRequestIdKey(string requestIdKey, string value) - { - Client.DefaultRequestHeaders.Add(requestIdKey, value); - ResponseMessage = Client.GetAsync(Url).Result; - } - - public void Dispose() - { - Client.Dispose(); - Server.Dispose(); - } - } -} diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs index 08b67820..60555435 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; using Shouldly; using TestStack.BDDfy; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ConsulServiceDiscoveryProviderTests.cs index a272d1b4..b60330d8 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ConsulServiceDiscoveryProviderTests.cs @@ -1,14 +1,16 @@ using System; using System.Collections.Generic; using System.IO; -using System.Text; +using System.Linq; using Consul; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Moq; +using Ocelot.Infrastructure.Consul; using Ocelot.Logging; -using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; using Xunit; using TestStack.BDDfy; @@ -20,14 +22,16 @@ namespace Ocelot.UnitTests.ServiceDiscovery { private IWebHost _fakeConsulBuilder; private readonly List _serviceEntries; - private readonly ConsulServiceDiscoveryProvider _provider; + private ConsulServiceDiscoveryProvider _provider; private readonly string _serviceName; private readonly int _port; private readonly string _consulHost; private readonly string _fakeConsulServiceDiscoveryUrl; private List _services; - private Mock _factory; + private readonly Mock _factory; private readonly Mock _logger; + private string _receivedToken; + private IConsulClientFactory _clientFactory; public ConsulServiceDiscoveryProviderTests() { @@ -38,11 +42,12 @@ namespace Ocelot.UnitTests.ServiceDiscovery _serviceEntries = new List(); _factory = new Mock(); + _clientFactory = new ConsulClientFactory(); _logger = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); - var config = new ConsulRegistryConfiguration(_consulHost, _port, _serviceName); - _provider = new ConsulServiceDiscoveryProvider(config, _factory.Object); + var config = new ConsulRegistryConfiguration(_consulHost, _port, _serviceName, null); + _provider = new ConsulServiceDiscoveryProvider(config, _factory.Object, _clientFactory); } [Fact] @@ -67,6 +72,33 @@ namespace Ocelot.UnitTests.ServiceDiscovery .BDDfy(); } + [Fact] + public void should_use_token() + { + var token = "test token"; + var config = new ConsulRegistryConfiguration(_consulHost, _port, _serviceName, token); + _provider = new ConsulServiceDiscoveryProvider(config, _factory.Object, _clientFactory); + + var serviceEntryOne = new ServiceEntry() + { + Service = new AgentService() + { + Service = _serviceName, + Address = "localhost", + Port = 50881, + ID = Guid.NewGuid().ToString(), + Tags = new string[0] + }, + }; + + this.Given(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(_fakeConsulServiceDiscoveryUrl, _serviceName)) + .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) + .When(_ => WhenIGetTheServices()) + .Then(_ => ThenTheCountIs(1)) + .And(_ => _receivedToken.ShouldBe(token)) + .BDDfy(); + } + [Fact] public void should_not_return_services_with_invalid_address() { @@ -140,12 +172,12 @@ namespace Ocelot.UnitTests.ServiceDiscovery private void ThenTheLoggerHasBeenCalledCorrectlyForInvalidAddress() { _logger.Verify( - x => x.LogError( + x => x.LogWarning( "Unable to use service Address: http://localhost and Port: 50881 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), Times.Once); _logger.Verify( - x => x.LogError( + x => x.LogWarning( "Unable to use service Address: http://localhost and Port: 50888 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), Times.Once); } @@ -153,12 +185,12 @@ namespace Ocelot.UnitTests.ServiceDiscovery private void ThenTheLoggerHasBeenCalledCorrectlyForInvalidPorts() { _logger.Verify( - x => x.LogError( + x => x.LogWarning( "Unable to use service Address: localhost and Port: -1 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), Times.Once); _logger.Verify( - x => x.LogError( + x => x.LogWarning( "Unable to use service Address: localhost and Port: 0 as it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"), Times.Once); } @@ -195,6 +227,11 @@ namespace Ocelot.UnitTests.ServiceDiscovery { if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") { + if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) + { + _receivedToken = values.First(); + } + await context.Response.WriteJsonAsync(_serviceEntries); } }); diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs new file mode 100644 index 00000000..0ec071d1 --- /dev/null +++ b/test/Ocelot.UnitTests/ServiceDiscovery/EurekaServiceDiscoveryProviderTests.cs @@ -0,0 +1,117 @@ +namespace Ocelot.UnitTests.ServiceDiscovery +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Moq; + using Ocelot.ServiceDiscovery.Providers; + using Pivotal.Discovery.Client; + using Shouldly; + using TestStack.BDDfy; + using Values; + using Xunit; + + public class EurekaServiceDiscoveryProviderTests + { + private readonly EurekaServiceDiscoveryProvider _provider; + private readonly Mock _client; + private readonly string _serviceId; + private List _instances; + private List _result; + + public EurekaServiceDiscoveryProviderTests() + { + _serviceId = "Laura"; + _client = new Mock(); + _provider = new EurekaServiceDiscoveryProvider(_serviceId, _client.Object); + } + + [Fact] + public void should_return_empty_services() + { + this.When(_ => WhenIGet()) + .Then(_ => ThenTheCountIs(0)) + .BDDfy(); + } + + [Fact] + public void should_return_service_from_client() + { + var instances = new List + { + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()) + }; + + this.Given(_ => GivenThe(instances)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheCountIs(1)) + .And(_ => ThenTheClientIsCalledCorrectly()) + .And(_ => ThenTheServiceIsMapped()) + .BDDfy(); + } + + [Fact] + public void should_return_services_from_client() + { + var instances = new List + { + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()), + new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()) + }; + + this.Given(_ => GivenThe(instances)) + .When(_ => WhenIGet()) + .Then(_ => ThenTheCountIs(2)) + .And(_ => ThenTheClientIsCalledCorrectly()) + .BDDfy(); + } + + private void ThenTheServiceIsMapped() + { + _result[0].HostAndPort.DownstreamHost.ShouldBe("somehost"); + _result[0].HostAndPort.DownstreamPort.ShouldBe(801); + _result[0].Name.ShouldBe(_serviceId); + } + + private void ThenTheCountIs(int expected) + { + _result.Count.ShouldBe(expected); + } + + private void ThenTheClientIsCalledCorrectly() + { + _client.Verify(x => x.GetInstances(_serviceId), Times.Once); + } + + private async Task WhenIGet() + { + _result = await _provider.Get(); + } + + private void GivenThe(List instances) + { + _instances = instances; + _client.Setup(x => x.GetInstances(It.IsAny())).Returns(instances); + } + } + + public class EurekaService : IServiceInstance + { + public EurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) + { + ServiceId = serviceId; + Host = host; + Port = port; + IsSecure = isSecure; + Uri = uri; + Metadata = metadata; + } + + public string ServiceId { get; } + public string Host { get; } + public int Port { get; } + public bool IsSecure { get; } + public Uri Uri { get; } + public IDictionary Metadata { get; } + } +} diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs index 39deb681..eabe72d7 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs @@ -1,4 +1,7 @@ -namespace Ocelot.UnitTests.ServiceDiscovery +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.ServiceDiscovery.Providers; + +namespace Ocelot.UnitTests.ServiceDiscovery { using System; using System.Collections.Generic; diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs index 021b9efb..4621913a 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceProviderFactoryTests.cs @@ -3,14 +3,18 @@ using System.Collections.Generic; using Moq; using Ocelot.Configuration; using Ocelot.Configuration.Builder; +using Ocelot.Infrastructure.Consul; using Ocelot.Logging; using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; using Shouldly; using TestStack.BDDfy; using Xunit; namespace Ocelot.UnitTests.ServiceDiscovery { + using Pivotal.Discovery.Client; + public class ServiceProviderFactoryTests { private ServiceProviderConfiguration _serviceConfig; @@ -18,11 +22,13 @@ namespace Ocelot.UnitTests.ServiceDiscovery private readonly ServiceDiscoveryProviderFactory _factory; private DownstreamReRoute _reRoute; private Mock _loggerFactory; + private Mock _discoveryClient; public ServiceProviderFactoryTests() { _loggerFactory = new Mock(); - _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object); + _discoveryClient = new Mock(); + _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, new ConsulClientFactory(), _discoveryClient.Object); } [Fact] @@ -86,7 +92,7 @@ namespace Ocelot.UnitTests.ServiceDiscovery .Build(); var serviceConfig = new ServiceProviderConfigurationBuilder() - .WithServiceDiscoveryProviderType("ServiceFabric") + .WithType("ServiceFabric") .Build(); this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) @@ -95,6 +101,24 @@ namespace Ocelot.UnitTests.ServiceDiscovery .BDDfy(); } + [Fact] + public void should_return_eureka_provider() + { + var reRoute = new DownstreamReRouteBuilder() + .WithServiceName("product") + .WithUseServiceDiscovery(true) + .Build(); + + var serviceConfig = new ServiceProviderConfigurationBuilder() + .WithType("Eureka") + .Build(); + + this.Given(x => x.GivenTheReRoute(serviceConfig, reRoute)) + .When(x => x.WhenIGetTheServiceProvider()) + .Then(x => x.ThenTheServiceProviderIs()) + .BDDfy(); + } + private void ThenTheFollowingServicesAreReturned(List downstreamAddresses) { var result = (ConfigurationServiceProvider)_result; diff --git a/test/Ocelot.UnitTests/configuration.json b/test/Ocelot.UnitTests/configuration.json deleted file mode 100755 index 618957b8..00000000 --- a/test/Ocelot.UnitTests/configuration.json +++ /dev/null @@ -1 +0,0 @@ -{"ReRoutes":[{"DownstreamPathTemplate":"/test/test/{test}","UpstreamPathTemplate":null,"UpstreamHttpMethod":null,"AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ApiName":null,"RequireHttps":false,"AllowedScopes":[],"ApiSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0},"ReRouteIsCaseSensitive":false,"ServiceName":null,"DownstreamScheme":"https","DownstreamHost":"localhost","DownstreamPort":80,"QoSOptions":{"ExceptionsAllowedBeforeBreaking":0,"DurationOfBreak":0,"TimeoutValue":0},"LoadBalancer":null}],"GlobalConfiguration":{"RequestIdKey":null,"ServiceDiscoveryProvider":{"Provider":"consul","Host":"blah","Port":198},"AdministrationPath":"testy"}} \ No newline at end of file diff --git a/test/Ocelot.UnitTests/idsrv3test.pfx b/test/Ocelot.UnitTests/idsrv3test.pfx deleted file mode 100644 index 0247dea0..00000000 Binary files a/test/Ocelot.UnitTests/idsrv3test.pfx and /dev/null differ