diff --git a/.gitignore b/.gitignore index 47975241f..d1f51a22c 100644 --- a/.gitignore +++ b/.gitignore @@ -232,7 +232,6 @@ ClientBin/ *.dbmdl *.dbproj.schemaview *.jfm -*.pfx *.publishsettings orleans.codegen.cs diff --git a/NuGet.config b/NuGet.config index 8bf285bae..27feda65e 100644 --- a/NuGet.config +++ b/NuGet.config @@ -5,7 +5,7 @@ - + diff --git a/README.md b/README.md index 954cceace..6bc439753 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ YARP is a reverse proxy toolkit for building fast proxy servers in .NET using th We expect YARP to ship as a library and project template that together provide a robust, performant proxy server. Its pipeline and modules are designed so that you can then customize the functionality for your needs. For example, while YARP supports configuration files, we expect that many users will want to manage the configuration programmatically based on their own backend configuration management system, YARP will provide a configuration API to enable that customization in-proc. YARP is designed with customizability as a primary scenario, rather than requiring you to break out to script or having to rebuild from source. -# Current Status +# Updates -For the latest status updates, see our [Status Report thread](https://github.com/microsoft/reverse-proxy/issues/97). Subscribe to notifications on that issue and we'll comment regularly with status updates. +For regular updates, see our [releases page](https://github.com/microsoft/reverse-proxy/releases). Subscribe to release notifications on this repository to be notified of future updates (Watch -> Custom -> Releases). # Build diff --git a/docs/docfx/articles/getting_started.md b/docs/docfx/articles/getting_started.md index 56e739228..051aecd17 100644 --- a/docs/docfx/articles/getting_started.md +++ b/docs/docfx/articles/getting_started.md @@ -7,7 +7,7 @@ title: Getting Started with YARP YARP is designed as a library that provides the core proxy functionality which you can then customize by adding or replacing modules. YARP is currently provided as a NuGet package and code snippets. We plan on providing a project template and pre-built exe in the future. -YARP 1.0.0 Preview 8 supports ASP.NET Core 3.1 and 5.0. You can download the .NET 5 SDK from https://dotnet.microsoft.com/download/dotnet/5.0. ASP.NET Core 5.0 on Windows requires Visual Studio 2019 v16.8 or newer. +YARP 1.0.0 Preview 9 supports ASP.NET Core 3.1 and 5.0. You can download the .NET 5 SDK from https://dotnet.microsoft.com/download/dotnet/5.0. ASP.NET Core 5.0 on Windows requires Visual Studio 2019 v16.8 or newer. ### Create a new project @@ -35,7 +35,7 @@ And then add: ```XML - + ``` diff --git a/docs/docfx/articles/session-affinity.md b/docs/docfx/articles/session-affinity.md index 280aaf67e..947e68ddb 100644 --- a/docs/docfx/articles/session-affinity.md +++ b/docs/docfx/articles/session-affinity.md @@ -7,7 +7,7 @@ Session affinity is a mechanism to bind (affinitize) a causally related request ## Configuration ### Services and middleware registration -Session affinity services are registered in the DI container via `AddSessionAffinityProvider()` method which is automatically called by `AddReverseProxy()`. The middleware `UseAffinitizedDestinationLookup()` and `UseRequestAffinitizer()` are included by default in the parameterless MapReverseProxy method. If you are customizing the proxy pipeline, place the first middleware **before** adding `LoadBalancingMiddleware` and the second **after** load balancing. +Session affinity services are registered in the DI container via `AddSessionAffinityProvider()` method which is automatically called by `AddReverseProxy()`. The middleware `UseAffinitizedDestinationLookup()` is included by default in the parameterless MapReverseProxy method. If you are customizing the proxy pipeline, place the first middleware **before** adding `LoadBalancingMiddleware` load balancing. Example: ```C# @@ -15,7 +15,6 @@ endpoints.MapReverseProxy(proxyPipeline => { proxyPipeline.UseAffinitizedDestinationLookup(); proxyPipeline.UseProxyLoadBalancing(); - proxyPipeline.UseRequestAffinitizer(); }); ``` @@ -77,4 +76,4 @@ There are two built-in failure policies. The default is `Redistribute`. The session affinity mechanisms are implemented by the services (mentioned above) and the two following middleware: 1. `AffinitizedDestinationLookupMiddleware` - coordinates the request's affinity resolution process. First, it calls a provider implementing the mode specified for the given cluster on `ClusterConfig.SessionAffinity.Mode` property. Then, it checks the affinity resolution status returned by the provider, and calls a failure handling policy set on `ClusterConfig.SessionAffinity.FailurePolicy` in case of failures. It must be added to the pipeline **before** the load balancer. -2. `AffinitizeRequestMiddleware` - sets the key on the response if a new affinity has been established for the request. Otherwise, if the request follows an existing affinity, it does nothing. It must be added to the pipeline **after** the load balancer. +2. `AffinitizeTransform` - sets the key on the response if a new affinity has been established for the request. Otherwise, if the request follows an existing affinity, it does nothing. This is automatically added as a response transform. diff --git a/docs/docfx/articles/transforms.md b/docs/docfx/articles/transforms.md index 334a9f033..be5591a3f 100644 --- a/docs/docfx/articles/transforms.md +++ b/docs/docfx/articles/transforms.md @@ -497,8 +497,8 @@ The {Prefix}PathBase header value is taken from `HttpContext.Request.PathBase`. | Key | Value | Default | Required | |-----|-------|---------|----------| | Forwarded | A comma separated list containing any of these values: for,by,proto,host | (none) | yes | -| ForFormat | Random/RandomAndPort/Unknown/UnknownAndPort/Ip/IpAndPort | Random | no | -| ByFormat | Random/RandomAndPort/Unknown/UnknownAndPort/Ip/IpAndPort | Random | no | +| ForFormat | Random/RandomAndPort/RandomAndRandomPort/Unknown/UnknownAndPort/UnknownAndRandomPort/Ip/IpAndPort/IpAndRandomPort | Random | no | +| ByFormat | Random/RandomAndPort/RandomAndRandomPort/Unknown/UnknownAndPort/UnknownAndRandomPort/Ip/IpAndPort/IpAndRandomPort | Random | no | | Append | true/false | true | no | Config: @@ -543,10 +543,13 @@ The RFC allows a [variety of formats](https://tools.ietf.org/html/rfc7239#sectio |--------|-------------|---------| | Random | An obfuscated identifier that is generated randomly per request. This allows for diagnostic tracing scenarios while limiting the flow of uniquely identifying information for privacy reasons. | `by=_YQuN68tm6` | | RandomAndPort | The Random identifier plus the port. | `by="_YQuN68tm6:80"` | +| RandomAndRandomPort | The Random identifier plus another random identifier for the port. | `by="_YQuN68tm6:_jDw5Cf3tQ"` | | Unknown | This can be used when the identity of the preceding entity is not known, but the proxy server still wants to signal that the request was forwarded. | `by=unknown` | | UnknownAndPort | The Unknown identifier plus the port if available. | `by="unknown:80"` | +| UnknownAndRandomPort | The Unknown identifier plus random identifier for the port. | `by="unknown:_jDw5Cf3tQ"` | | Ip | An IPv4 address or an IPv6 address including brackets. | `by="[::1]"` | | IpAndPort | The IP address plus the port. | `by="[::1]:80"` | +| IpAndRandomPort | The IP address plus random identifier for the port. | `by="[::1]:_jDw5Cf3tQ"` | ### ClientCert @@ -686,15 +689,15 @@ ResponseTrailer follows the same structure and guidance as ResponseHeader. ### AddRequestTransform -[AddRequestTransform](xref:Microsoft.ReverseProxy.Abstractions.Config.TransformBuilderContextFuncExtensions) is a `TransformBuilderContext` extension method that defines a request transform as a `Func`. This allows creating a custom request transform without implementing a `RequestTransform` derived class. +[AddRequestTransform](xref:Microsoft.ReverseProxy.Abstractions.Config.TransformBuilderContextFuncExtensions) is a `TransformBuilderContext` extension method that defines a request transform as a `Func`. This allows creating a custom request transform without implementing a `RequestTransform` derived class. ### AddResponseTransform -[AddResponseTransform](xref:Microsoft.ReverseProxy.Abstractions.Config.TransformBuilderContextFuncExtensions) is a `TransformBuilderContext` extension method that defines a response transform as a `Func`. This allows creating a custom response transform without implementing a `ResponseTransform` derived class. +[AddResponseTransform](xref:Microsoft.ReverseProxy.Abstractions.Config.TransformBuilderContextFuncExtensions) is a `TransformBuilderContext` extension method that defines a response transform as a `Func`. This allows creating a custom response transform without implementing a `ResponseTransform` derived class. ### AddResponseTrailersTransform -[AddResponseTrailersTransform](xref:Microsoft.ReverseProxy.Abstractions.Config.TransformBuilderContextFuncExtensions) is a `TransformBuilderContext` extension method that defines a response trailers transform as a `Func`. This allows creating a custom response trailers transform without implementing a `ResponseTrailersTransform` derived class. +[AddResponseTrailersTransform](xref:Microsoft.ReverseProxy.Abstractions.Config.TransformBuilderContextFuncExtensions) is a `TransformBuilderContext` extension method that defines a response trailers transform as a `Func`. This allows creating a custom response trailers transform without implementing a `ResponseTrailersTransform` derived class. ### RequestTransform @@ -755,7 +758,7 @@ services.AddReverseProxy() transformBuildContext.AddRequestTransform(transformContext => { transformContext.ProxyRequest.Headers.Add("CustomHeader", value); - return Task.CompletedTask; + return default; }); } } @@ -810,7 +813,7 @@ services.AddReverseProxy() context.AddRequestTransform(transformContext => { transformContext.ProxyRequest.Headers.Add("CustomHeader", value); - return Task.CompletedTask; + return default; }); return true; // Matched diff --git a/docs/docfx/readme.md b/docs/docfx/readme.md index 314cedc1b..325f8e094 100644 --- a/docs/docfx/readme.md +++ b/docs/docfx/readme.md @@ -14,6 +14,6 @@ The build will produce a series of HTML files in the `_site` directory. Many of The docs are automatically built and published by a [GitHub Action](https://github.com/microsoft/reverse-proxy/blob/main/.github/workflows/docfx_build.yml) on every push to `release/docs`. The built `_site` directory is pushed to the `gh-pages` branch and served by [https://microsoft.github.io/reverse-proxy/](https://microsoft.github.io/reverse-proxy/). Maintaining a seperate branch for the released docs allows us to choose when to publish them and with what content, and without modifying the build scripts each release. -Doc edits for the current public release should go into that release's branch (e.g. `release/1.0.0-preview3`) and merged forward into `main`. Then `release/docs` should be reset to that release branch's position. +Doc edits for the current public release should go into that release's branch (e.g. `release/1.0.0-preview3`) and merged forward into `main` and `release/docs`. When publishing a new product version (e.g. `release/1.0.0-preview4`) `release/docs` should be reset to that position after the docs have been updated. diff --git a/docs/operations/BackportingToPreview.md b/docs/operations/BackportingToPreview.md new file mode 100644 index 000000000..bc4ab457c --- /dev/null +++ b/docs/operations/BackportingToPreview.md @@ -0,0 +1,21 @@ +# Backporting changes to a preview branch + +Backporting changes is very similar to a regular release. Changes are made on the preview branch, the builds are validated and ultimately released. + +- Checkout the preview branch + + `git checkout release/1.0.0-previewX` +- Make and commit any changes +- Push the changes **to your own fork** and submit a PR against the preview branch +- Once the PR is merged, wait for the internal [`microsoft-reverse-proxy-official`](https://dev.azure.com/dnceng/internal/_build?definitionId=809&_a=summary&view=branches) pipeline to produce a build +- Validate the build the same way you would for a regular release [docs](https://github.com/microsoft/reverse-proxy/blob/main/docs/operations/Release.md#validate-the-final-build) +- Package Artifacts from this build can be shared to validate the patch. Optionally, the artifacts from the [public pipeline](https://dev.azure.com/dnceng/public/_build?definitionId=807&view=branches) can be used +- Continue iterating on the preview branch until satisfied with the validation of the change +- [Release the build](https://github.com/microsoft/reverse-proxy/blob/main/docs/operations/Release.md#release-the-build) from the preview branch +- Update the preview tag to the released commit + + **While still on the preview branch:** + - `git tag -d v1.0.0-previewX` (delete the current tag) + - `git tag v1.0.0-previewX` (re-create the tag on the current commit) + - `git push upstream --tags --force` (force push the tag change to the upstream repo (**not your fork**)) +- Update the description of the [release](https://github.com/microsoft/reverse-proxy/releases) if necessary. The associated tag/commit will be automatically updated by the previous step. \ No newline at end of file diff --git a/docs/operations/Branching.md b/docs/operations/Branching.md index c7bbd4f3b..f1dcdedef 100644 --- a/docs/operations/Branching.md +++ b/docs/operations/Branching.md @@ -1,167 +1,17 @@ # Branching Tasks -*This documentation is primarily for project maintainers, though contributors are welcome to read and learn about our process!* - -We are aiming to ship YARP previews aligned with .NET 5, since we are working on changes to the runtime that will improve the experience for YARP. As part of that, our schedule for a preview milestone has three phases: - -1. Open development and receiving new builds of .NET 5 - Commits are going to `main` and new builds of .NET 5 are coming regularly -2. Open development, .NET 5 builds frozen - .NET 5 branches about a month before release, so after they branch we will switch `main` to receive builds from that release branch. Our development will continue until approximately a week before release. -3. Branch and prepare for release - Prior to the release date, we'll branch and prepare for release. Note: in practice we haven't needed to create the release branch until the day before we plan to release. We've also not needed to set up dependency flow in our release branches because the runtime has finished producing new builds that close to release. Main always targets the runtime channel for the next release. - -## Scheduling - -We need to ship versions of our package that depend upon the **released** preview builds. In order to ensure we do that, we try to ship same-date or shortly after .NET 5 previews (see the [.NET Wiki (Internal)](https://aka.ms/dotnet-wiki) for schedule details). - -## Dependency Flow Overview - -*For full documentation on Arcade, Maestro and `darc`, see [the Arcade documentation](https://github.com/dotnet/arcade/tree/master/Documentation)* - -We use the .NET Engineering System ([Arcade](https://github.com/dotnet/arcade)) to build this repo, since many of the contributors are part of the .NET team and we want to use nightly builds of .NET 5. Part of the engineering system is a service called "Maestro" which manages dependency flow between repositories. When one repository finishes building, it can automatically publish it's build to a Maestro "Channel". Other repos can subscribe to that channel to receive updated builds. Maestro will automatically open a PR to update dependencies in repositories that are subscribed to changes in dependent repositories. - -Maestro can be queried and controlled using the `darc` command line tool. To use `darc` you will need to be a member of the [`dotnet/arcade-contrib` GitHub Team](https://github.com/orgs/dotnet/teams/arcade-contrib). To set up `darc`: - -1. Run `.\eng\common\darc-init.ps1` to install the global tool. -2. Once installed, run `darc authenticate` and follow the instructions in the file opened in your editor to set up the necessary access token for Maestro. You should *only* need the Maestro token for the commands used here, but feel free to configure the other tokens as well. -3. Save and close the file, and `darc` will be ready to go. - -Running `darc` with no args will show a list of commands. The `darc help [command]` command will give you help on a specific command. - -Repositories can be configured to publish builds automatically to a certain channel, based on the branch. For example, most .NET repos are set up like this: - -* Builds out of `main` are auto-published to the `.NET 5 Dev` channel -* Builds out of `release/5.0.0-preview.X` are auto-published to the `.NET 5 Preview X` channel (where `X` is some preview number) - -To see the current mappings for a repository, you can run `darc get-default-channels --source-repo [repo]`, where `[repo]` is any substring that matches a full GitHub URL for a repo in the system. The easiest way to use `[repo]` is to just specify the `[owner]/[name]` form for a repo. For example: - -```shell -> darc get-default-channels --source-repo dotnet/aspnetcore -(912) https://github.com/dotnet/aspnetcore @ release/3.1 -> .NET Core 3.1 Release -(913) https://github.com/dotnet/aspnetcore @ main -> .NET 5 Dev -(1160) https://github.com/dotnet/aspnetcore @ faster-publishing -> General Testing -(1003) https://github.com/dotnet/aspnetcore @ wtgodbe/NonStablev2 -> General Testing -(1089) https://github.com/dotnet/aspnetcore @ generate-akams-links -> General Testing -(1021) https://github.com/dotnet/aspnetcore @ NonStablePackageVersion -> General Testing -(1018) https://github.com/dotnet/aspnetcore @ wtgodbe/Checksum3x -> General Testing -(1281) https://github.com/dotnet/aspnetcore @ wtgodbe/FixChecksums -> General Testing -(914) https://github.com/dotnet/aspnetcore @ blazor-wasm -> .NET Core 3.1 Blazor Features -(1252) https://github.com/dotnet/aspnetcore @ release/5.0-preview3 -> .NET 5 Preview 3 -(1301) https://github.com/dotnet/aspnetcore @ release/5.0-preview4 -> .NET 5 Preview 4 -(916) https://github.com/dotnet/aspnetcore-tooling @ release/3.1 -> .NET Core 3.1 Release -(917) https://github.com/dotnet/aspnetcore-tooling @ master -> .NET 5 Dev -(1178) https://github.com/dotnet/aspnetcore-tooling @ release/vs16.6-preview2 -> General Testing -(1282) https://github.com/dotnet/aspnetcore-tooling @ wtgodbe/DontPublishDebug -> General Testing -(1253) https://github.com/dotnet/aspnetcore-tooling @ release/5.0-preview3 -> .NET 5 Preview 3 -(1302) https://github.com/dotnet/aspnetcore-tooling @ release/5.0-preview4 -> .NET 5 Preview 4 -``` - -Subscriptions are managed using the `get-subscriptions`, `add-subscription` and `update-subscription` commands. You can view all subscriptions in the system by running `darc get-subscription`. You can also filter subscriptions by the source and target using the `--source-repo [repo]` and `--target-repo [repo]` arguments. For example, to see everything that `microsoft/reverse-proxy` is subscribed to: - -```shell -> darc get-subscriptions --target-repo microsoft/reverse-proxy -https://github.com/dotnet/arcade (.NET Eng - Latest) ==> 'https://github.com/microsoft/reverse-proxy' ('main') - - Id: 642e03bf-3679-4569-fcfc-08d7d0f045ee - - Update Frequency: EveryWeek - - Enabled: True - - Batchable: False - - Merge Policies: - Standard -https://github.com/dotnet/runtime (.NET 5 Preview 4) ==> 'https://github.com/microsoft/reverse-proxy' ('main') - - Id: 763f49c1-8016-44b6-8810-08d7e1727af8 - - Update Frequency: EveryBuild - - Enabled: True - - Batchable: False - - Merge Policies: - Standard -``` - -To add a new subscription, run `darc add-subscription` with no arguments. An editor window will open with a TODO script like this: - -``` -Channel: -Source Repository URL: -Target Repository URL: -Target Branch: -Update Frequency: <'none', 'everyDay', 'everyBuild', 'twiceDaily', 'everyWeek'> -Batchable: False -Merge Policies: [] -``` - -A number of comments will also be present, describing available values and what they do. Fill these fields in, for example: - -``` -Channel: .NET 5 Dev -Source Repository URL: https://github.com/dotnet/runtime -Target Repository URL: https://github.com/microsoft/reverse-proxy -Target Branch: main -Update Frequency: everyBuild -Batchable: False -Merge Policies: -- Name: Standard -``` - -Save and exit the editor and the subscription will be created. - -Similarly, you can edit an existing subscription by using `darc update-subscription --id [ID]` (get the `[ID]` value from `get-subscriptions`). This will open the same TODO script, but with the current values filled in. Just update them, then save and exit to update. - -## Prerequisites - -* Properly configured `darc` global tool, configured with a Maestro authentication token. - -## When .NET 5 branches for a preview - -When .NET 5 branches for a preview, we need to switch the "channel" from which we are getting builds. See [Dependency Flow Onboarding](https://github.com/dotnet/arcade/blob/master/Documentation/DependencyFlowOnboarding.md) for more information on channels. - -To do this, run the following commands: - -1. Run `darc get-subscriptions --target-repo microsoft/reverse-proxy --source-repo dotnet/runtime` to get the subscription from `dotnet/runtime` to `microsoft/reverse-proxy` -2. Copy the `Id` value -3. Run `darc update-subscription --id [Id]`. An editor will open. -4. Change the value of the `Channel` field to `.NET 5 Preview X` (where `X` is the preview we are remaining on) -4. Change the value of the `Update Frequency` field to `EveryBuild` (since there should be relatively few builds and we want to be sure we're on the latest). -5. Save and close the file in your editor, `darc` will proceed to make the update. -6. Answer `y` to `Trigger this subscription immediately?` and a PR will be opened to update versions to the latest ones in that channel. -7. Merge the PR as soon as feasible. - -## When we are ready to branch - When we are ready to branch our code, we first need to create the branch: 1. In a local clone, run `git checkout main` and `git pull origin main` to make sure you have the latest `main` 2. Run `git checkout -b release/1.0.0-previewX` where `X` is the YARP preview number. 3. Run `git push origin release/1.0.0-previewX` to push the branch to the server. -Then, set up dependency flow so we continue getting new runtime bits if necessary: - -1. Run `darc add-subscription` -2. Fill in the template that opens in your editor as follows: - * `Channel` = `.NET 5 Preview X` where `X` is the .NET 5 preview that matches the YARP preview - * `Source Repository URL` = `https://github.com/dotnet/runtime` - * `Target Repository URL` = `https://github.com/microsoft/reverse-proxy` - * `Target Branch` = `release/1.0.0-previewX` (where `X` is the YARP preview number; this should be the same as the branch you created above) - * `Update Frequency` = `everyBuild` (Builds will be rare in this channel and we'll want every one) - * `Merge Policies` is a multiline value, it should look like this: - -``` -Merge Policies: -- Name: Standard - Properties: {} -``` - -3. Save and close the editor window. - -Restore the `main` branch to pulling the latest bits from .NET 5: - -1. Run `darc get-subscriptions --target-repo microsoft/reverse-proxy --source-repo dotnet/runtime` to get the subscription from `dotnet/runtime` to `microsoft/reverse-proxy` -2. Copy the `Id` value -3. Run `darc update-subscription --id [Id]`. An editor will open. -4. Change the value of the `Channel` field to `.NET 5 Dev` -4. Change the value of the `Update Frequency` field back to `EveryWeek` to reduce PR noise. -5. Save and close the file in your editor, `darc` will proceed to make the update. -6. Answer `y` to `Trigger this subscription immediately?` and a PR will be opened to update versions to the latest ones in that channel. -7. Merge the PR as soon as feasible. - -Finally, update branding in `main`: +Update branding in `main`: 1. Edit the file [`eng/Version.props`](../../eng/Version.props) 2. Set `PreReleaseVersionLabel` to `preview.X` (where `X` is the next preview number) 3. Send a PR and merge it ASAP (auto-merge is your friend). + +Update the runtimes and SDKs in `global.json` in `main`: + +Check that the global.json includes the latest 3.1 runtime versions from [here](https://dotnet.microsoft.com/download/dotnet-core/3.1), and the 5.0 SDK version from [here](https://dotnet.microsoft.com/download/dotnet/5.0). \ No newline at end of file diff --git a/docs/operations/DependencyFlow.md b/docs/operations/DependencyFlow.md new file mode 100644 index 000000000..5b5bf4ee5 --- /dev/null +++ b/docs/operations/DependencyFlow.md @@ -0,0 +1,159 @@ +# ***Note that this document may be outdated as .NET 5 has already been released*** + +It is kept here as it may be useful in the future (.NET 6.0) + +# Dependency Flow + +We are aiming to ship YARP previews aligned with .NET 5, since we are working on changes to the runtime that will improve the experience for YARP. As part of that, our schedule for a preview milestone has three phases: + +1. Open development and receiving new builds of .NET 5 - Commits are going to `main` and new builds of .NET 5 are coming regularly +2. Open development, .NET 5 builds frozen - .NET 5 branches about a month before release, so after they branch we will switch `main` to receive builds from that release branch. Our development will continue until approximately a week before release. +3. Branch and prepare for release - Prior to the release date, we'll branch and prepare for release. Note: in practice we haven't needed to create the release branch until the day before we plan to release. We've also not needed to set up dependency flow in our release branches because the runtime has finished producing new builds that close to release. Main always targets the runtime channel for the next release. + +## Scheduling + +We need to ship versions of our package that depend upon the **released** preview builds. In order to ensure we do that, we try to ship same-date or shortly after .NET 5 previews (see the [.NET Wiki (Internal)](https://aka.ms/dotnet-wiki) for schedule details). + +## Dependency Flow Overview + +*For full documentation on Arcade, Maestro and `darc`, see [the Arcade documentation](https://github.com/dotnet/arcade/tree/master/Documentation)* + +We use the .NET Engineering System ([Arcade](https://github.com/dotnet/arcade)) to build this repo, since many of the contributors are part of the .NET team and we want to use nightly builds of .NET 5. Part of the engineering system is a service called "Maestro" which manages dependency flow between repositories. When one repository finishes building, it can automatically publish it's build to a Maestro "Channel". Other repos can subscribe to that channel to receive updated builds. Maestro will automatically open a PR to update dependencies in repositories that are subscribed to changes in dependent repositories. + +Maestro can be queried and controlled using the `darc` command line tool. To use `darc` you will need to be a member of the [`dotnet/arcade-contrib` GitHub Team](https://github.com/orgs/dotnet/teams/arcade-contrib). To set up `darc`: + +1. Run `.\eng\common\darc-init.ps1` to install the global tool. +2. Once installed, run `darc authenticate` and follow the instructions in the file opened in your editor to set up the necessary access token for Maestro. You should *only* need the Maestro token for the commands used here, but feel free to configure the other tokens as well. +3. Save and close the file, and `darc` will be ready to go. + +Running `darc` with no args will show a list of commands. The `darc help [command]` command will give you help on a specific command. + +Repositories can be configured to publish builds automatically to a certain channel, based on the branch. For example, most .NET repos are set up like this: + +* Builds out of `main` are auto-published to the `.NET 5 Dev` channel +* Builds out of `release/5.0.0-preview.X` are auto-published to the `.NET 5 Preview X` channel (where `X` is some preview number) + +To see the current mappings for a repository, you can run `darc get-default-channels --source-repo [repo]`, where `[repo]` is any substring that matches a full GitHub URL for a repo in the system. The easiest way to use `[repo]` is to just specify the `[owner]/[name]` form for a repo. For example: + +```shell +> darc get-default-channels --source-repo dotnet/aspnetcore +(912) https://github.com/dotnet/aspnetcore @ release/3.1 -> .NET Core 3.1 Release +(913) https://github.com/dotnet/aspnetcore @ main -> .NET 5 Dev +(1160) https://github.com/dotnet/aspnetcore @ faster-publishing -> General Testing +(1003) https://github.com/dotnet/aspnetcore @ wtgodbe/NonStablev2 -> General Testing +(1089) https://github.com/dotnet/aspnetcore @ generate-akams-links -> General Testing +(1021) https://github.com/dotnet/aspnetcore @ NonStablePackageVersion -> General Testing +(1018) https://github.com/dotnet/aspnetcore @ wtgodbe/Checksum3x -> General Testing +(1281) https://github.com/dotnet/aspnetcore @ wtgodbe/FixChecksums -> General Testing +(914) https://github.com/dotnet/aspnetcore @ blazor-wasm -> .NET Core 3.1 Blazor Features +(1252) https://github.com/dotnet/aspnetcore @ release/5.0-preview3 -> .NET 5 Preview 3 +(1301) https://github.com/dotnet/aspnetcore @ release/5.0-preview4 -> .NET 5 Preview 4 +(916) https://github.com/dotnet/aspnetcore-tooling @ release/3.1 -> .NET Core 3.1 Release +(917) https://github.com/dotnet/aspnetcore-tooling @ master -> .NET 5 Dev +(1178) https://github.com/dotnet/aspnetcore-tooling @ release/vs16.6-preview2 -> General Testing +(1282) https://github.com/dotnet/aspnetcore-tooling @ wtgodbe/DontPublishDebug -> General Testing +(1253) https://github.com/dotnet/aspnetcore-tooling @ release/5.0-preview3 -> .NET 5 Preview 3 +(1302) https://github.com/dotnet/aspnetcore-tooling @ release/5.0-preview4 -> .NET 5 Preview 4 +``` + +Subscriptions are managed using the `get-subscriptions`, `add-subscription` and `update-subscription` commands. You can view all subscriptions in the system by running `darc get-subscription`. You can also filter subscriptions by the source and target using the `--source-repo [repo]` and `--target-repo [repo]` arguments. For example, to see everything that `microsoft/reverse-proxy` is subscribed to: + +```shell +> darc get-subscriptions --target-repo microsoft/reverse-proxy +https://github.com/dotnet/arcade (.NET Eng - Latest) ==> 'https://github.com/microsoft/reverse-proxy' ('main') + - Id: 642e03bf-3679-4569-fcfc-08d7d0f045ee + - Update Frequency: EveryWeek + - Enabled: True + - Batchable: False + - Merge Policies: + Standard +https://github.com/dotnet/runtime (.NET 5 Preview 4) ==> 'https://github.com/microsoft/reverse-proxy' ('main') + - Id: 763f49c1-8016-44b6-8810-08d7e1727af8 + - Update Frequency: EveryBuild + - Enabled: True + - Batchable: False + - Merge Policies: + Standard +``` + +To add a new subscription, run `darc add-subscription` with no arguments. An editor window will open with a TODO script like this: + +``` +Channel: +Source Repository URL: +Target Repository URL: +Target Branch: +Update Frequency: <'none', 'everyDay', 'everyBuild', 'twiceDaily', 'everyWeek'> +Batchable: False +Merge Policies: [] +``` + +A number of comments will also be present, describing available values and what they do. Fill these fields in, for example: + +``` +Channel: .NET 5 Dev +Source Repository URL: https://github.com/dotnet/runtime +Target Repository URL: https://github.com/microsoft/reverse-proxy +Target Branch: main +Update Frequency: everyBuild +Batchable: False +Merge Policies: +- Name: Standard +``` + +Save and exit the editor and the subscription will be created. + +Similarly, you can edit an existing subscription by using `darc update-subscription --id [ID]` (get the `[ID]` value from `get-subscriptions`). This will open the same TODO script, but with the current values filled in. Just update them, then save and exit to update. + +## Prerequisites + +* Properly configured `darc` global tool, configured with a Maestro authentication token. + +## When .NET 5 branches for a preview + +When .NET 5 branches for a preview, we need to switch the "channel" from which we are getting builds. See [Dependency Flow Onboarding](https://github.com/dotnet/arcade/blob/master/Documentation/DependencyFlowOnboarding.md) for more information on channels. + +To do this, run the following commands: + +1. Run `darc get-subscriptions --target-repo microsoft/reverse-proxy --source-repo dotnet/runtime` to get the subscription from `dotnet/runtime` to `microsoft/reverse-proxy` +2. Copy the `Id` value +3. Run `darc update-subscription --id [Id]`. An editor will open. +4. Change the value of the `Channel` field to `.NET 5 Preview X` (where `X` is the preview we are remaining on) +4. Change the value of the `Update Frequency` field to `EveryBuild` (since there should be relatively few builds and we want to be sure we're on the latest). +5. Save and close the file in your editor, `darc` will proceed to make the update. +6. Answer `y` to `Trigger this subscription immediately?` and a PR will be opened to update versions to the latest ones in that channel. +7. Merge the PR as soon as feasible. + +## When we are ready to branch + +Follow the initial branching steps from [Branching.md](Branching.md). + +Set up dependency flow so we continue getting new runtime bits if necessary: + +1. Run `darc add-subscription` +2. Fill in the template that opens in your editor as follows: + * `Channel` = `.NET 5 Preview X` where `X` is the .NET 5 preview that matches the YARP preview + * `Source Repository URL` = `https://github.com/dotnet/runtime` + * `Target Repository URL` = `https://github.com/microsoft/reverse-proxy` + * `Target Branch` = `release/1.0.0-previewX` (where `X` is the YARP preview number; this should be the same as the branch you created above) + * `Update Frequency` = `everyBuild` (Builds will be rare in this channel and we'll want every one) + * `Merge Policies` is a multiline value, it should look like this: + +``` +Merge Policies: +- Name: Standard + Properties: {} +``` + +3. Save and close the editor window. + +Restore the `main` branch to pulling the latest bits from .NET 5: + +1. Run `darc get-subscriptions --target-repo microsoft/reverse-proxy --source-repo dotnet/runtime` to get the subscription from `dotnet/runtime` to `microsoft/reverse-proxy` +2. Copy the `Id` value +3. Run `darc update-subscription --id [Id]`. An editor will open. +4. Change the value of the `Channel` field to `.NET 5 Dev` +4. Change the value of the `Update Frequency` field back to `EveryWeek` to reduce PR noise. +5. Save and close the file in your editor, `darc` will proceed to make the update. +6. Answer `y` to `Trigger this subscription immediately?` and a PR will be opened to update versions to the latest ones in that channel. +7. Merge the PR as soon as feasible. \ No newline at end of file diff --git a/docs/operations/README.md b/docs/operations/README.md index e30727c38..3d48a3832 100644 --- a/docs/operations/README.md +++ b/docs/operations/README.md @@ -1,7 +1,11 @@ # Operations Playbook +*This documentation is primarily for project maintainers, though contributers are welcome to read and learn about our process!* + This section has docs on various operations and tasks to be performed in the repo. * [Writing Status Updates](StatusReport.md) * [Branching](Branching.md) -* [Releasing](Release.md) \ No newline at end of file +* [Releasing](Release.md) +* [Backporting changes onto a preview branch](BackportingToPreview.md) +* [Dependency flow](DependencyFlow.md) \ No newline at end of file diff --git a/docs/operations/Release.md b/docs/operations/Release.md index 5c8e990b1..5ca488e1f 100644 --- a/docs/operations/Release.md +++ b/docs/operations/Release.md @@ -2,9 +2,14 @@ This document provides a guide on how to release a preview of YARP. +To keep track of the process, open a [release checklist issue](https://github.com/microsoft/reverse-proxy/issues/new?title=Preview%20X%20release%20checklist&body=-%20%5B%20%5D%20Ensure%20there%27s%20a%20release%20branch%20created%20%28see%20%5BBranching%5D%28https%3A%2F%2Fgithub.com%2Fmicrosoft%2Freverse-proxy%2Fblob%2Fmain%2Fdocs%2Foperations%2FBranching.md%29%29%0A-%20%5B%20%5D%20Ensure%20the%20%60Version.props%60%20has%20the%20%60PreReleaseVersionLabel%60%20updated%20to%20the%20next%20preview%0A-%20%5B%20%5D%20Identify%20and%20validate%20the%20build%20on%20the%20%60microsoft-reverse-proxy-official%60%20pipeline%0A-%20%5B%20%5D%20Release%20the%20build%0A-%20%5B%20%5D%20Tag%20the%20commit%0A-%20%5B%20%5D%20Draft%20release%20notes%0A-%20%5B%20%5D%20Publish%20the%20docs%0A-%20%5B%20%5D%20Publish%20release%20notes%0A-%20%5B%20%5D%20Close%20the%20%5Bold%20milestone%5D%28https%3A%2F%2Fgithub.com%2Fmicrosoft%2Freverse-proxy%2Fmilestones%29%0A-%20%5B%20%5D%20Announce%20on%20social%20media%0A-%20%5B%20%5D%20Set%20the%20preview%20branch%20to%20protected%0A-%20%5B%20%5D%20Delete%20the%20%5Bprevious%20preview%20branch%5D%28https%3A%2F%2Fgithub.com%2Fmicrosoft%2Freverse-proxy%2Fbranches%29). + ## Ensure there's a release branch created. -See [Branching](Branching.md). +See [Branching](Branching.md): +- Make the next preview branch. +- Update the branding in main. +- Update the global.json runtime and SDK versions in main. ## Identify the Final Build @@ -12,8 +17,6 @@ First, identify the final build of the [`microsoft-reverse-proxy-official` Azure Once you've identified that build, click in to the build details. - - ## Validate the Final Build At this point, you can perform any validation that makes sense. At a minimum, we should validate that the sample can run with the candidate packages. You can download the final build using the "Artifacts" which can be accessed under "Related" in the header: @@ -68,12 +71,22 @@ The packages will be pushed and when the "NuGet.org" stage turns green, the pack ## Tag the commit -Create and push a git tag for the commit associated with the final build (not necessarily the HEAD of the current release branch). See prior tags for the preferred format. Use a lightweight tag, not annotated, e.g.: `git tag v1.0.0-preview6`. +Create and push a git tag for the commit associated with the final build (not necessarily the HEAD of the current release branch). See prior tags for the preferred format. Use a lightweight tag, not annotated. + +`git tag v1.0.0-previewX` + +Push the tag change to the upstream repo (**not your fork**) + +`git push upstream --tags` ## Draft release notes Create a draft release at https://github.com/microsoft/reverse-proxy/releases using the new tag. See prior releases for the recommended content and format. +## Update references to the package version +Some samples may include the package version as part of the references in the project file, which makes it easier to clone and run. Those should be updated to the new package version that has just been published. Locations include: +1. [BasicYarpSample.csproj](../../samples/BasicYarpSample/BasicYarpSample.csproj) + ## Publish the docs Reset the `release/docs` branch to the head of the current preview branch to publish the latest docs. See [docs](../docfx/readme.md). @@ -90,12 +103,13 @@ It should be empty now. If it's not, move the outstanding issues to the next one David Fowler has a lot of twitter followers interested in YARP. Tweet a link to the release notes and let him retweet it. -## Complete any outstanding branching tasks. +## Set the preview branch to protected + +This is to avoid accidental pushes to/deletions of the preview branch. + +## Delete the previous preview branch -See [Branching](Branching.md). -- Make sure the versions in main have been updated for the next milestone. -- Update the runtime dependency flow with DARC -- Update the SDK +There should only be one [preview branch on the repo](https://github.com/microsoft/reverse-proxy/branches) after this point. ## Troubleshooting diff --git a/eng/Build.props b/eng/Build.props index a3bda2e3f..90acf47bb 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -1,9 +1,12 @@ - + - \ No newline at end of file + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index ffacdaee9..6faf0bda4 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -7,13 +7,13 @@ - + https://github.com/dotnet/arcade - 3233c41c837f72827efd8f827b538e047334847d + 938b3e8b4edcd96ca0f0cbbae63c87b3f51f7afe - + https://github.com/dotnet/arcade - 3233c41c837f72827efd8f827b538e047334847d + 938b3e8b4edcd96ca0f0cbbae63c87b3f51f7afe diff --git a/eng/Versions.props b/eng/Versions.props index 8ef34557c..73143962f 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -7,7 +7,7 @@ That ensures that if we get up to preview 10, it doesn't sort as between preview 1 and preview 2! See https://semver.org/spec/v2.0.0.html for details. --> - preview.9 + preview.10 diff --git a/eng/common/dotnet-install.sh b/eng/common/dotnet-install.sh index 25b69f878..d6efeb443 100755 --- a/eng/common/dotnet-install.sh +++ b/eng/common/dotnet-install.sh @@ -49,13 +49,8 @@ while [[ $# > 0 ]]; do shift done -# Use uname to determine what the CPU is. -cpuname=$(uname -p) -# Some Linux platforms report unknown for platform, but the arch for machine. -if [[ "$cpuname" == "unknown" ]]; then - cpuname=$(uname -m) -fi - +# Use uname to determine what the CPU is, see https://en.wikipedia.org/wiki/Uname#Examples +cpuname=$(uname -m) case $cpuname in aarch64) buildarch=arm64 diff --git a/eng/common/internal-feed-operations.ps1 b/eng/common/internal-feed-operations.ps1 index b8f6529fd..418c09930 100644 --- a/eng/common/internal-feed-operations.ps1 +++ b/eng/common/internal-feed-operations.ps1 @@ -63,8 +63,6 @@ function SetupCredProvider { } if (($endpoints | Measure-Object).Count -gt 0) { - # [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Endpoint code example with no real credentials.")] - # Create the JSON object. It should look like '{"endpointCredentials": [{"endpoint":"http://example.index.json", "username":"optional", "password":"accesstoken"}]}' $endpointCredentials = @{endpointCredentials=$endpoints} | ConvertTo-Json -Compress # Create the environment variables the AzDo way diff --git a/eng/common/internal-feed-operations.sh b/eng/common/internal-feed-operations.sh index a27410f63..e2233e781 100755 --- a/eng/common/internal-feed-operations.sh +++ b/eng/common/internal-feed-operations.sh @@ -62,8 +62,6 @@ function SetupCredProvider { endpoints+=']' if [ ${#endpoints} -gt 2 ]; then - # [SuppressMessage("Microsoft.Security", "CS002:SecretInNextLine", Justification="Endpoint code example with no real credentials.")] - # Create the JSON object. It should look like '{"endpointCredentials": [{"endpoint":"http://example.index.json", "username":"optional", "password":"accesstoken"}]}' local endpointCredentials="{\"endpointCredentials\": "$endpoints"}" echo "##vso[task.setvariable variable=VSS_NUGET_EXTERNAL_FEED_ENDPOINTS]$endpointCredentials" diff --git a/eng/common/templates/post-build/post-build.yml b/eng/common/templates/post-build/post-build.yml index b9aa5b605..822ff5975 100644 --- a/eng/common/templates/post-build/post-build.yml +++ b/eng/common/templates/post-build/post-build.yml @@ -62,7 +62,8 @@ parameters: VS168ChannelId: 1154 VSMasterChannelId: 1012 VS169ChannelId: 1473 - + VS1610ChannelId: 1692 + stages: - ${{ if or(and(le(parameters.publishingInfraVersion, 2), eq(parameters.inline, 'true')), eq( parameters.enableNugetValidation, 'true'), eq(parameters.enableSigningValidation, 'true'), eq(parameters.enableSourceLinkValidation, 'true'), eq(parameters.SDLValidationParameters.enable, 'true')) }}: - stage: Validate @@ -91,7 +92,7 @@ stages: inputs: filePath: $(Build.SourcesDirectory)/eng/common/post-build/check-channel-consistency.ps1 arguments: -PromoteToChannels "$(TargetChannels)" - -AvailableChannelIds ${{parameters.NetEngLatestChannelId}},${{parameters.NetEngValidationChannelId}},${{parameters.NetDev5ChannelId}},${{parameters.NetDev6ChannelId}},${{parameters.GeneralTestingChannelId}},${{parameters.NETCoreToolingDevChannelId}},${{parameters.NETCoreToolingReleaseChannelId}},${{parameters.NETInternalToolingChannelId}},${{parameters.NETCoreExperimentalChannelId}},${{parameters.NetEngServicesIntChannelId}},${{parameters.NetEngServicesProdChannelId}},${{parameters.NetCoreSDK313xxChannelId}},${{parameters.NetCoreSDK313xxInternalChannelId}},${{parameters.NetCoreSDK314xxChannelId}},${{parameters.NetCoreSDK314xxInternalChannelId}},${{parameters.VS166ChannelId}},${{parameters.VS167ChannelId}},${{parameters.VS168ChannelId}},${{parameters.VSMasterChannelId}},${{parameters.VS169ChannelId}} + -AvailableChannelIds ${{parameters.NetEngLatestChannelId}},${{parameters.NetEngValidationChannelId}},${{parameters.NetDev5ChannelId}},${{parameters.NetDev6ChannelId}},${{parameters.GeneralTestingChannelId}},${{parameters.NETCoreToolingDevChannelId}},${{parameters.NETCoreToolingReleaseChannelId}},${{parameters.NETInternalToolingChannelId}},${{parameters.NETCoreExperimentalChannelId}},${{parameters.NetEngServicesIntChannelId}},${{parameters.NetEngServicesProdChannelId}},${{parameters.NetCoreSDK313xxChannelId}},${{parameters.NetCoreSDK313xxInternalChannelId}},${{parameters.NetCoreSDK314xxChannelId}},${{parameters.NetCoreSDK314xxInternalChannelId}},${{parameters.VS166ChannelId}},${{parameters.VS167ChannelId}},${{parameters.VS168ChannelId}},${{parameters.VSMasterChannelId}},${{parameters.VS169ChannelId}},${{parameters.VS1610ChannelId}} - job: displayName: NuGet Validation @@ -568,3 +569,18 @@ stages: transportFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools-transport/nuget/v3/index.json' shippingFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json' symbolsFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools-symbols/nuget/v3/index.json' + + - template: \eng\common\templates\post-build\channels\generic-public-channel.yml + parameters: + BARBuildId: ${{ parameters.BARBuildId }} + PromoteToChannelIds: ${{ parameters.PromoteToChannelIds }} + artifactsPublishingAdditionalParameters: ${{ parameters.artifactsPublishingAdditionalParameters }} + dependsOn: ${{ parameters.publishDependsOn }} + publishInstallersAndChecksums: ${{ parameters.publishInstallersAndChecksums }} + symbolPublishingAdditionalParameters: ${{ parameters.symbolPublishingAdditionalParameters }} + stageName: 'VS_16_10_Publishing' + channelName: 'VS 16.10' + channelId: ${{ parameters.VS1610ChannelId }} + transportFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools-transport/nuget/v3/index.json' + shippingFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json' + symbolsFeed: 'https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools-symbols/nuget/v3/index.json' diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 10e98593e..572da3b9f 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -508,7 +508,7 @@ function InitializeBuildTool() { ExitWithExitCode 1 } $dotnetPath = Join-Path $dotnetRoot (GetExecutableFileName 'dotnet') - $buildTool = @{ Path = $dotnetPath; Command = 'msbuild'; Tool = 'dotnet'; Framework = 'netcoreapp2.1' } + $buildTool = @{ Path = $dotnetPath; Command = 'msbuild'; Tool = 'dotnet'; Framework = 'netcoreapp3.1' } } elseif ($msbuildEngine -eq "vs") { try { $msbuildPath = InitializeVisualStudioMSBuild -install:$restore @@ -644,13 +644,26 @@ function MSBuild() { } $toolsetBuildProject = InitializeToolset - $path = Split-Path -parent $toolsetBuildProject - $path = Join-Path $path (Join-Path $buildTool.Framework 'Microsoft.DotNet.ArcadeLogging.dll') - if (-not (Test-Path $path)) { - $path = Split-Path -parent $toolsetBuildProject - $path = Join-Path $path (Join-Path $buildTool.Framework 'Microsoft.DotNet.Arcade.Sdk.dll') + $basePath = Split-Path -parent $toolsetBuildProject + $possiblePaths = @( + # new scripts need to work with old packages, so we need to look for the old names/versions + (Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.ArcadeLogging.dll')), + (Join-Path $basePath (Join-Path $buildTool.Framework 'Microsoft.DotNet.Arcade.Sdk.dll')), + (Join-Path $basePath (Join-Path netcoreapp2.1 'Microsoft.DotNet.ArcadeLogging.dll')), + (Join-Path $basePath (Join-Path netcoreapp2.1 'Microsoft.DotNet.Arcade.Sdk.dll')) + ) + $selectedPath = $null + foreach ($path in $possiblePaths) { + if (Test-Path $path -PathType Leaf) { + $selectedPath = $path + break + } + } + if (-not $selectedPath) { + Write-PipelineTelemetryError -Category 'Build' -Message 'Unable to find arcade sdk logger assembly.' + ExitWithExitCode 1 } - $args += "/logger:$path" + $args += "/logger:$selectedPath" } MSBuild-Core @args diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 0920f5965..9019b7f5c 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -307,7 +307,7 @@ function InitializeBuildTool { # return values _InitializeBuildTool="$_InitializeDotNetCli/dotnet" _InitializeBuildToolCommand="msbuild" - _InitializeBuildToolFramework="netcoreapp2.1" + _InitializeBuildToolFramework="netcoreapp3.1" } # Set RestoreNoCache as a workaround for https://github.com/NuGet/Home/issues/3116 @@ -414,11 +414,24 @@ function MSBuild { fi local toolset_dir="${_InitializeToolset%/*}" - local logger_path="$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.ArcadeLogging.dll" - if [[ ! -f $logger_path ]]; then - logger_path="$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.Arcade.Sdk.dll" + # new scripts need to work with old packages, so we need to look for the old names/versions + local selectedPath= + local possiblePaths=() + possiblePaths+=( "$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.ArcadeLogging.dll" ) + possiblePaths+=( "$toolset_dir/$_InitializeBuildToolFramework/Microsoft.DotNet.Arcade.Sdk.dll" ) + possiblePaths+=( "$toolset_dir/netcoreapp2.1/Microsoft.DotNet.ArcadeLogging.dll" ) + possiblePaths+=( "$toolset_dir/netcoreapp2.1/Microsoft.DotNet.Arcade.Sdk.dll" ) + for path in "${possiblePaths[@]}"; do + if [[ -f $path ]]; then + selectedPath=$path + break + fi + done + if [[ -z "$selectedPath" ]]; then + Write-PipelineTelemetryError -category 'Build' "Unable to find arcade sdk logger assembly." + ExitWithExitCode 1 fi - args=( "${args[@]}" "-logger:$logger_path" ) + args+=( "-logger:$selectedPath" ) fi MSBuild-Core ${args[@]} diff --git a/global.json b/global.json index e4ffd64a0..c601d9f49 100644 --- a/global.json +++ b/global.json @@ -1,28 +1,28 @@ { "sdk": { - "version": "5.0.100" + "version": "5.0.103" }, "tools": { - "dotnet": "5.0.100", + "dotnet": "5.0.103", "runtimes": { "dotnet/x64": [ "$(MicrosoftNETCoreAppPackageVersion)", - "3.1.8" + "3.1.12" ], "dotnet/x86": [ "$(MicrosoftNETCoreAppPackageVersion)", - "3.1.8" + "3.1.12" ], "aspnetcore/x64": [ - "3.1.8" + "3.1.12" ], "aspnetcore/x86": [ - "3.1.8" + "3.1.12" ] } }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21078.12", - "Microsoft.DotNet.Helix.Sdk": "6.0.0-beta.21078.12" + "Microsoft.DotNet.Arcade.Sdk": "6.0.0-beta.21105.12", + "Microsoft.DotNet.Helix.Sdk": "6.0.0-beta.21105.12" } } diff --git a/reverse-proxy.sln b/reverse-proxy.sln index 8ad803487..c14f4d35e 100644 --- a/reverse-proxy.sln +++ b/reverse-proxy.sln @@ -14,8 +14,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ReverseProxy", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ReverseProxy.Tests", "test\ReverseProxy.Tests\Microsoft.ReverseProxy.Tests.csproj", "{3B188E4C-C926-4BED-94F3-0E83668FBAB0}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleClient", "samples\SampleClient\SampleClient.csproj", "{D294FD0A-CF8E-4DB2-B722-08577DB890D2}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleServer", "samples\SampleServer\SampleServer.csproj", "{11D098B2-7116-4F37-817A-E496B8F15C76}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{149C61A2-D9F8-49B9-9F9B-3C953FEF53AA}" @@ -28,8 +26,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{BC344A50-8 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "docfx", "docs\docfx\docfx.csproj", "{7F6D4710-07D1-49D0-8EAE-675A3A9B50E3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkApp", "samples\BenchmarkApp\BenchmarkApp.csproj", "{F663FD56-C01D-4FA7-96A4-B1217ED95E3C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReverseProxy.Auth.Sample", "samples\ReverseProxy.Auth.Sample\ReverseProxy.Auth.Sample.csproj", "{354F2755-A090-4735-A657-726FB6DA92CD}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ReverseProxy.FunctionalTests", "test\ReverseProxy.FunctionalTests\Microsoft.ReverseProxy.FunctionalTests.csproj", "{31089146-71DA-45C2-ACA6-EA1E2C916FD0}" @@ -50,6 +46,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReverseProxy.ServiceFabric. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.ReverseProxy.Telemetry.Consumption", "src\ReverseProxy.TelemetryConsumption\Microsoft.ReverseProxy.Telemetry.Consumption.csproj", "{9EE0CBFE-54E0-4189-AF30-BE0BE3185D2A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "testassets", "testassets", "{CDB73246-0A7E-4116-81E0-828228ECADDD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkApp", "testassets\BenchmarkApp\BenchmarkApp.csproj", "{8C5F5323-4317-4A83-BB10-784FAAF7C9EB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReverseProxy.Code", "testassets\ReverseProxy.Code\ReverseProxy.Code.csproj", "{9931B4F1-FBE9-4F7D-A5AB-CCE92F340ED2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReverseProxy.Config", "testassets\ReverseProxy.Config\ReverseProxy.Config.csproj", "{9E3466B8-7263-4ADF-85D8-2412F73F64E1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReverseProxy.Metrics.Sample", "samples\ReverseProxy.Metrics.Sample\ReverseProxy.Metrics.Sample.csproj", "{41C5ED9C-E700-4270-ABAB-2159DD02EF61}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestClient", "testassets\TestClient\TestClient.csproj", "{07C042A4-2E76-49BE-8AAF-5904470BEC69}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicYarpSample", "samples\BasicYarpSample\BasicYarpSample.csproj", "{FA62D919-758B-4A84-8F70-393A47A077B3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReverseProxy.Transforms.Sample", "samples\ReverseProxy.Transforms.Sample\ReverseProxy.Transforms.Sample.csproj", "{AD5C2956-760C-4A3F-9894-C1590BEB8D54}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestServer", "testassets\TestServer\TestServer.csproj", "{AE06F9BE-0B8D-4D02-9DC6-FE5A41FA4B5D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -74,14 +88,6 @@ Global {3B188E4C-C926-4BED-94F3-0E83668FBAB0}.Release|Any CPU.Build.0 = Release|Any CPU {3B188E4C-C926-4BED-94F3-0E83668FBAB0}.Release|x64.ActiveCfg = Release|Any CPU {3B188E4C-C926-4BED-94F3-0E83668FBAB0}.Release|x64.Build.0 = Release|Any CPU - {D294FD0A-CF8E-4DB2-B722-08577DB890D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D294FD0A-CF8E-4DB2-B722-08577DB890D2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D294FD0A-CF8E-4DB2-B722-08577DB890D2}.Debug|x64.ActiveCfg = Debug|Any CPU - {D294FD0A-CF8E-4DB2-B722-08577DB890D2}.Debug|x64.Build.0 = Debug|Any CPU - {D294FD0A-CF8E-4DB2-B722-08577DB890D2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D294FD0A-CF8E-4DB2-B722-08577DB890D2}.Release|Any CPU.Build.0 = Release|Any CPU - {D294FD0A-CF8E-4DB2-B722-08577DB890D2}.Release|x64.ActiveCfg = Release|Any CPU - {D294FD0A-CF8E-4DB2-B722-08577DB890D2}.Release|x64.Build.0 = Release|Any CPU {11D098B2-7116-4F37-817A-E496B8F15C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {11D098B2-7116-4F37-817A-E496B8F15C76}.Debug|Any CPU.Build.0 = Debug|Any CPU {11D098B2-7116-4F37-817A-E496B8F15C76}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -98,14 +104,6 @@ Global {7F6D4710-07D1-49D0-8EAE-675A3A9B50E3}.Release|Any CPU.Build.0 = Release|Any CPU {7F6D4710-07D1-49D0-8EAE-675A3A9B50E3}.Release|x64.ActiveCfg = Release|Any CPU {7F6D4710-07D1-49D0-8EAE-675A3A9B50E3}.Release|x64.Build.0 = Release|Any CPU - {F663FD56-C01D-4FA7-96A4-B1217ED95E3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F663FD56-C01D-4FA7-96A4-B1217ED95E3C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F663FD56-C01D-4FA7-96A4-B1217ED95E3C}.Debug|x64.ActiveCfg = Debug|Any CPU - {F663FD56-C01D-4FA7-96A4-B1217ED95E3C}.Debug|x64.Build.0 = Debug|Any CPU - {F663FD56-C01D-4FA7-96A4-B1217ED95E3C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F663FD56-C01D-4FA7-96A4-B1217ED95E3C}.Release|Any CPU.Build.0 = Release|Any CPU - {F663FD56-C01D-4FA7-96A4-B1217ED95E3C}.Release|x64.ActiveCfg = Release|Any CPU - {F663FD56-C01D-4FA7-96A4-B1217ED95E3C}.Release|x64.Build.0 = Release|Any CPU {354F2755-A090-4735-A657-726FB6DA92CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {354F2755-A090-4735-A657-726FB6DA92CD}.Debug|Any CPU.Build.0 = Debug|Any CPU {354F2755-A090-4735-A657-726FB6DA92CD}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -186,6 +184,70 @@ Global {9EE0CBFE-54E0-4189-AF30-BE0BE3185D2A}.Release|Any CPU.Build.0 = Release|Any CPU {9EE0CBFE-54E0-4189-AF30-BE0BE3185D2A}.Release|x64.ActiveCfg = Release|Any CPU {9EE0CBFE-54E0-4189-AF30-BE0BE3185D2A}.Release|x64.Build.0 = Release|Any CPU + {8C5F5323-4317-4A83-BB10-784FAAF7C9EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C5F5323-4317-4A83-BB10-784FAAF7C9EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C5F5323-4317-4A83-BB10-784FAAF7C9EB}.Debug|x64.ActiveCfg = Debug|Any CPU + {8C5F5323-4317-4A83-BB10-784FAAF7C9EB}.Debug|x64.Build.0 = Debug|Any CPU + {8C5F5323-4317-4A83-BB10-784FAAF7C9EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C5F5323-4317-4A83-BB10-784FAAF7C9EB}.Release|Any CPU.Build.0 = Release|Any CPU + {8C5F5323-4317-4A83-BB10-784FAAF7C9EB}.Release|x64.ActiveCfg = Release|Any CPU + {8C5F5323-4317-4A83-BB10-784FAAF7C9EB}.Release|x64.Build.0 = Release|Any CPU + {9931B4F1-FBE9-4F7D-A5AB-CCE92F340ED2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9931B4F1-FBE9-4F7D-A5AB-CCE92F340ED2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9931B4F1-FBE9-4F7D-A5AB-CCE92F340ED2}.Debug|x64.ActiveCfg = Debug|Any CPU + {9931B4F1-FBE9-4F7D-A5AB-CCE92F340ED2}.Debug|x64.Build.0 = Debug|Any CPU + {9931B4F1-FBE9-4F7D-A5AB-CCE92F340ED2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9931B4F1-FBE9-4F7D-A5AB-CCE92F340ED2}.Release|Any CPU.Build.0 = Release|Any CPU + {9931B4F1-FBE9-4F7D-A5AB-CCE92F340ED2}.Release|x64.ActiveCfg = Release|Any CPU + {9931B4F1-FBE9-4F7D-A5AB-CCE92F340ED2}.Release|x64.Build.0 = Release|Any CPU + {9E3466B8-7263-4ADF-85D8-2412F73F64E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E3466B8-7263-4ADF-85D8-2412F73F64E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9E3466B8-7263-4ADF-85D8-2412F73F64E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {9E3466B8-7263-4ADF-85D8-2412F73F64E1}.Debug|x64.Build.0 = Debug|Any CPU + {9E3466B8-7263-4ADF-85D8-2412F73F64E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9E3466B8-7263-4ADF-85D8-2412F73F64E1}.Release|Any CPU.Build.0 = Release|Any CPU + {9E3466B8-7263-4ADF-85D8-2412F73F64E1}.Release|x64.ActiveCfg = Release|Any CPU + {9E3466B8-7263-4ADF-85D8-2412F73F64E1}.Release|x64.Build.0 = Release|Any CPU + {41C5ED9C-E700-4270-ABAB-2159DD02EF61}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41C5ED9C-E700-4270-ABAB-2159DD02EF61}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41C5ED9C-E700-4270-ABAB-2159DD02EF61}.Debug|x64.ActiveCfg = Debug|Any CPU + {41C5ED9C-E700-4270-ABAB-2159DD02EF61}.Debug|x64.Build.0 = Debug|Any CPU + {41C5ED9C-E700-4270-ABAB-2159DD02EF61}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41C5ED9C-E700-4270-ABAB-2159DD02EF61}.Release|Any CPU.Build.0 = Release|Any CPU + {41C5ED9C-E700-4270-ABAB-2159DD02EF61}.Release|x64.ActiveCfg = Release|Any CPU + {41C5ED9C-E700-4270-ABAB-2159DD02EF61}.Release|x64.Build.0 = Release|Any CPU + {07C042A4-2E76-49BE-8AAF-5904470BEC69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07C042A4-2E76-49BE-8AAF-5904470BEC69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07C042A4-2E76-49BE-8AAF-5904470BEC69}.Debug|x64.ActiveCfg = Debug|Any CPU + {07C042A4-2E76-49BE-8AAF-5904470BEC69}.Debug|x64.Build.0 = Debug|Any CPU + {07C042A4-2E76-49BE-8AAF-5904470BEC69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07C042A4-2E76-49BE-8AAF-5904470BEC69}.Release|Any CPU.Build.0 = Release|Any CPU + {07C042A4-2E76-49BE-8AAF-5904470BEC69}.Release|x64.ActiveCfg = Release|Any CPU + {07C042A4-2E76-49BE-8AAF-5904470BEC69}.Release|x64.Build.0 = Release|Any CPU + {FA62D919-758B-4A84-8F70-393A47A077B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA62D919-758B-4A84-8F70-393A47A077B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA62D919-758B-4A84-8F70-393A47A077B3}.Debug|x64.ActiveCfg = Debug|Any CPU + {FA62D919-758B-4A84-8F70-393A47A077B3}.Debug|x64.Build.0 = Debug|Any CPU + {FA62D919-758B-4A84-8F70-393A47A077B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA62D919-758B-4A84-8F70-393A47A077B3}.Release|Any CPU.Build.0 = Release|Any CPU + {FA62D919-758B-4A84-8F70-393A47A077B3}.Release|x64.ActiveCfg = Release|Any CPU + {FA62D919-758B-4A84-8F70-393A47A077B3}.Release|x64.Build.0 = Release|Any CPU + {AD5C2956-760C-4A3F-9894-C1590BEB8D54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD5C2956-760C-4A3F-9894-C1590BEB8D54}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD5C2956-760C-4A3F-9894-C1590BEB8D54}.Debug|x64.ActiveCfg = Debug|Any CPU + {AD5C2956-760C-4A3F-9894-C1590BEB8D54}.Debug|x64.Build.0 = Debug|Any CPU + {AD5C2956-760C-4A3F-9894-C1590BEB8D54}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD5C2956-760C-4A3F-9894-C1590BEB8D54}.Release|Any CPU.Build.0 = Release|Any CPU + {AD5C2956-760C-4A3F-9894-C1590BEB8D54}.Release|x64.ActiveCfg = Release|Any CPU + {AD5C2956-760C-4A3F-9894-C1590BEB8D54}.Release|x64.Build.0 = Release|Any CPU + {AE06F9BE-0B8D-4D02-9DC6-FE5A41FA4B5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE06F9BE-0B8D-4D02-9DC6-FE5A41FA4B5D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE06F9BE-0B8D-4D02-9DC6-FE5A41FA4B5D}.Debug|x64.ActiveCfg = Debug|Any CPU + {AE06F9BE-0B8D-4D02-9DC6-FE5A41FA4B5D}.Debug|x64.Build.0 = Debug|Any CPU + {AE06F9BE-0B8D-4D02-9DC6-FE5A41FA4B5D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE06F9BE-0B8D-4D02-9DC6-FE5A41FA4B5D}.Release|Any CPU.Build.0 = Release|Any CPU + {AE06F9BE-0B8D-4D02-9DC6-FE5A41FA4B5D}.Release|x64.ActiveCfg = Release|Any CPU + {AE06F9BE-0B8D-4D02-9DC6-FE5A41FA4B5D}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -193,10 +255,8 @@ Global GlobalSection(NestedProjects) = preSolution {568EF8AE-7624-490D-A19F-C25D076FF091} = {6CBE18D4-64E9-492B-BB02-58CD57126C10} {3B188E4C-C926-4BED-94F3-0E83668FBAB0} = {0631147E-34BB-456D-B214-5B202C516D5C} - {D294FD0A-CF8E-4DB2-B722-08577DB890D2} = {149C61A2-D9F8-49B9-9F9B-3C953FEF53AA} {11D098B2-7116-4F37-817A-E496B8F15C76} = {149C61A2-D9F8-49B9-9F9B-3C953FEF53AA} {7F6D4710-07D1-49D0-8EAE-675A3A9B50E3} = {BC344A50-8F81-4762-9F4B-12714693144B} - {F663FD56-C01D-4FA7-96A4-B1217ED95E3C} = {149C61A2-D9F8-49B9-9F9B-3C953FEF53AA} {354F2755-A090-4735-A657-726FB6DA92CD} = {149C61A2-D9F8-49B9-9F9B-3C953FEF53AA} {31089146-71DA-45C2-ACA6-EA1E2C916FD0} = {0631147E-34BB-456D-B214-5B202C516D5C} {F2547357-FB2F-4944-842F-D33D1E7A17FC} = {149C61A2-D9F8-49B9-9F9B-3C953FEF53AA} @@ -207,6 +267,14 @@ Global {FC33B6BC-E328-45CD-8589-AF3A5D9450EE} = {0631147E-34BB-456D-B214-5B202C516D5C} {46DC2693-5E60-48B4-AC15-329692E2A252} = {149C61A2-D9F8-49B9-9F9B-3C953FEF53AA} {9EE0CBFE-54E0-4189-AF30-BE0BE3185D2A} = {6CBE18D4-64E9-492B-BB02-58CD57126C10} + {8C5F5323-4317-4A83-BB10-784FAAF7C9EB} = {CDB73246-0A7E-4116-81E0-828228ECADDD} + {9931B4F1-FBE9-4F7D-A5AB-CCE92F340ED2} = {CDB73246-0A7E-4116-81E0-828228ECADDD} + {9E3466B8-7263-4ADF-85D8-2412F73F64E1} = {CDB73246-0A7E-4116-81E0-828228ECADDD} + {41C5ED9C-E700-4270-ABAB-2159DD02EF61} = {149C61A2-D9F8-49B9-9F9B-3C953FEF53AA} + {07C042A4-2E76-49BE-8AAF-5904470BEC69} = {CDB73246-0A7E-4116-81E0-828228ECADDD} + {FA62D919-758B-4A84-8F70-393A47A077B3} = {149C61A2-D9F8-49B9-9F9B-3C953FEF53AA} + {AD5C2956-760C-4A3F-9894-C1590BEB8D54} = {149C61A2-D9F8-49B9-9F9B-3C953FEF53AA} + {AE06F9BE-0B8D-4D02-9DC6-FE5A41FA4B5D} = {CDB73246-0A7E-4116-81E0-828228ECADDD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {31F6924A-E427-4830-96E9-B47CEB7BFE78} diff --git a/samples/BasicYarpSample/BasicYarpSample.csproj b/samples/BasicYarpSample/BasicYarpSample.csproj new file mode 100644 index 000000000..f832c164d --- /dev/null +++ b/samples/BasicYarpSample/BasicYarpSample.csproj @@ -0,0 +1,11 @@ + + + + net5.0 + + + + + + + diff --git a/samples/BasicYarpSample/Program.cs b/samples/BasicYarpSample/Program.cs new file mode 100644 index 000000000..211b3d8ee --- /dev/null +++ b/samples/BasicYarpSample/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + + +namespace BasicYARPSample +{ + public class Program + { + public static void Main(string[] args) + { + // Create a Kestrel web server, and tell it to use the Startup class + // for the service configuration + var myHostBuilder = Host.CreateDefaultBuilder(args); + myHostBuilder.ConfigureWebHostDefaults(webHostBuilder => + { + webHostBuilder.UseStartup(); + }); + var myHost = myHostBuilder.Build(); + myHost.Run(); + } + } +} diff --git a/samples/BasicYarpSample/Properties/launchSettings.json b/samples/BasicYarpSample/Properties/launchSettings.json new file mode 100644 index 000000000..6a76dd25e --- /dev/null +++ b/samples/BasicYarpSample/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "YARP Proxy Sample": { + "commandName": "Project", + "launchBrowser": false, + "launchUrl": "", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/BasicYarpSample/README.md b/samples/BasicYarpSample/README.md new file mode 100644 index 000000000..8dfe48b94 --- /dev/null +++ b/samples/BasicYarpSample/README.md @@ -0,0 +1,62 @@ +# Basic YARP Sample + +This sample shows how to consume the YARP Library to produce a simple reverse proxy server. +The proxy server is implemented a plugin component for ASP.NET Core applications. ASP.NET Core servers like Kestrel provide the front end for the proxy by listening for http requests and then passing them to the proxy for paths that the proxy has registered. The proxy handles the requests by: +- Mapping the request URL path to a route in proxy configuration. +- Routes are mapped to clusters which are a collection of destination endpoints. +- The destinations are filtered based on health status, and session affinity (not used in this sample). +- From the remaining destinations, one is selected using a load balancing algorithm. +- The request is proxied to that destination. + +This sample reads its configuration from the [appsettings.json](appsettings.json) file which defines 2 routes and clusters: + +- *AnExample* - this route has a path of *{\*\*catch-all}* which means that it will match any path, unless there is another more specific route. + It routes to a cluster named *example* which has a single destination of http://example.com +- *route2* - this route matches a path of */something/{\*any}* which means that it will match any path that begins with "/something/". + As its a more specific route, it will match before the route above, even though it is listed second. + This routes to a cluster named *cluster2* with 2 destinations. + It will load balance between those 2 destinations using a Power of two choices algorithm. + That algorithm is best with more than 2 choices, but shows how to specify an algorithm in config. + +**Note:** The destination addresses used in the sample are using DNS names rather than IP addresses, this is so that the sample can be run and used without further changes. In a typical deployment, the destination servers should be specified with protocol, IP & ports, such as "https://123.4.5.6:7890" + +The proxy will listen to HTTP requests on port 5000, and HTTPS on port 5001. These are changable via the URLs property in config, and can be limited to just one protocol if required. + +## Files +- [BasicYarpSample.csproj](BasicYarpSample.csproj) - A C# project file (conceptually similar to a make file) that tells it to target the .NET 5 runtime, and to reference the proxy library from [nuget](https://www.nuget.org/packages/Microsoft.ReverseProxy/) (.NET's package manager). +- [Program.cs](Program.cs) - Provides the main entrypoint for .NET which uses an WebHostBuilder to initialize the server which listens for http requests. Typically, this file does not need to be modified for any proxy scenarios. +- [Startup.cs](Startup.cs) - Provides a class that is used to configure and control how http requests are handled by the server. In this sample, it does the bare minimum of: + - Adding proxy functionality to the services collection. + - Specifying that the proxy configuration will come from the config file (altrenatively it could be specified via code). + - Telling ASP.NET to use its routing service, to register the routes from YARP into its routing table, and use YARP to handle those requests. +- [appsettings.json](appsettings.json) - The configuration file for the .NET app, including sections for Kestrel, logging and the YARP proxy configuration. +- [Properties/launchsettings.json](Properties/launchsettings.json) - A configuration file used by Visual Studio to tell it how to start the app when debugging. + +## Getting started + +### Command line + +* Download and install the .NET SDK (free) from https://dotnet.microsoft.com/download if not already installed. Versions are available for Windows, Linux and MacOS. +* Clone or extract a zip of the sample files. +* Use ```dotnet run``` either within the sample folder or passing in the path to the .csproj file to start the server. +* File change notification is used for the appsettings.json file so changes can be made on the fly. + + +### Visual Studio Code +* Download and install Visual Studio Code (free) from https://code.visualstudio.com/ - versions are available for Windows, Linux and MacOS. +* Download and install the .NET SDK from https://dotnet.microsoft.com/download if not already installed. Versions are available for Windows, Linux and MacOS. +* Open the folder for the sample in VS Code (File->Open Folder). +* Press F5 to debug, or Ctrl + F5 to run the sample without debugging. + +### Visual Studio + +* Download and install Visual Studio from https://visualstudio.microsoft.com/ - versions are available for Windows and MacOS, including a free community edition. +* Open the project file. +* Press F5 to debug, or Ctrl + F5 to run the sample without debugging. + +## Things to try +- Change the ports the proxy listens on using the URLs property in configuration or on the command line. +- Change the routes and destinations used by the proxy. +- A web server sample is available in the [SampleServer](../SampleServer) folder. It will output the request headers as part of the response body so they can be examined with a browser. + - The URLs the server listens to can be changed on the command line, so that multiple instances can be run. + eg ```dotnet run ../SampleServer --Urls "http://localhost:10000;https://localhost:10010"``` diff --git a/samples/BasicYarpSample/Startup.cs b/samples/BasicYarpSample/Startup.cs new file mode 100644 index 000000000..b589c2db6 --- /dev/null +++ b/samples/BasicYarpSample/Startup.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace BasicYARPSample +{ + // Sets up the ASP.NET application with the reverse proxy enabled. + public class Startup + { + public Startup(IConfiguration configuration) + { + // Default configuration comes from AppSettings.json file in project/output + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add capabilities to + // the web application via services in the DI container. + public void ConfigureServices(IServiceCollection services) + { + // Add the reverse proxy to capability to the server + var proxyBuilder = services.AddReverseProxy(); + // Initialize the reverse proxy from the "ReverseProxy" section of configuration + proxyBuilder.LoadFromConfig(Configuration.GetSection("ReverseProxy")); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request + // pipeline that handles requests + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + // Enable endpoint routing, required for the reverse proxy + app.UseRouting(); + // Register the reverse proxy routes + app.UseEndpoints(endpoints => + { + endpoints.MapReverseProxy(); + }); + } + } +} diff --git a/samples/BasicYarpSample/appsettings.json b/samples/BasicYarpSample/appsettings.json new file mode 100644 index 000000000..db9fdb15a --- /dev/null +++ b/samples/BasicYarpSample/appsettings.json @@ -0,0 +1,54 @@ +{ + // Base URLs the server listens on, must be configured independently of the routes below + "Urls": "http://localhost:6000;https://localhost:6001", + "Logging": { + "LogLevel": { + "Default": "Information", + // Uncomment to hide diagnostic messages from runtime and proxy + // "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "ReverseProxy": { + // Routes tell the proxy which requests to forward + "Routes": [ + { + // Matches anything and routes it to www.example.com + "RouteId": "AnExample", + "ClusterId": "example", + "Match": { + "Path": "{**catch-all}" + } + }, + { + // matches /something/* and routes to 2 external addresses + "RouteId": "route2", + "ClusterId": "cluster2", + "Match": { + "Path": "/something/{*any}" + } + } + ], + // Clusters tell the proxy where and how to forward requests + "Clusters": { + "example": { + "Destinations": { + "example.com": { + "Address": "http://www.example.com/" + } + } + }, + "cluster2": { + "Destinations": { + "first_destination": { + "Address": "https://contoso.com" + }, + "another_destination": { + "Address": "https://bing.com" + } + }, + "LoadBalancingPolicy" : "PowerOfTwoChoices" + } + } + } +} diff --git a/samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj b/samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj index 181416bc9..9438e4a95 100644 --- a/samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj +++ b/samples/ReverseProxy.Auth.Sample/ReverseProxy.Auth.Sample.csproj @@ -1,4 +1,4 @@ - + net5.0;netcoreapp3.1 @@ -7,7 +7,7 @@ - + diff --git a/samples/ReverseProxy.Auth.Sample/appsettings.json b/samples/ReverseProxy.Auth.Sample/appsettings.json index fd616b51c..a7a815291 100644 --- a/samples/ReverseProxy.Auth.Sample/appsettings.json +++ b/samples/ReverseProxy.Auth.Sample/appsettings.json @@ -22,7 +22,7 @@ "cluster1": { "Destinations": { "cluster1/destination1": { - "Address": "https://localhost:10000/" + "Address": "https://example.com/" } } } diff --git a/samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj b/samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj index a1032320a..979994a5b 100644 --- a/samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj +++ b/samples/ReverseProxy.Code.Sample/ReverseProxy.Code.Sample.csproj @@ -7,8 +7,7 @@ - - + diff --git a/samples/ReverseProxy.Code.Sample/Startup.cs b/samples/ReverseProxy.Code.Sample/Startup.cs index 196a6c0f7..26ce66efc 100644 --- a/samples/ReverseProxy.Code.Sample/Startup.cs +++ b/samples/ReverseProxy.Code.Sample/Startup.cs @@ -3,14 +3,10 @@ using System; using System.Collections.Generic; -using System.Net.Http.Headers; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.ReverseProxy.Abstractions; -using Microsoft.ReverseProxy.Abstractions.Config; using Microsoft.ReverseProxy.Middleware; -using Microsoft.ReverseProxy.Telemetry.Consumption; namespace Microsoft.ReverseProxy.Sample { @@ -24,7 +20,7 @@ public class Startup /// public void ConfigureServices(IServiceCollection services) { - services.AddControllers(); + // Manually create routes and cluster configs. This allows loading the data from an arbitrary source. var routes = new[] { new ProxyRoute() @@ -33,6 +29,7 @@ public void ConfigureServices(IServiceCollection services) ClusterId = "cluster1", Match = new ProxyMatch { + // Path or Hosts are required for each route. This catch-all pattern matches all request paths. Path = "{**catch-all}" } } @@ -45,40 +42,14 @@ public void ConfigureServices(IServiceCollection services) SessionAffinity = new SessionAffinityOptions { Enabled = true, Mode = "Cookie" }, Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "destination1", new Destination() { Address = "https://localhost:10000" } } + { "destination1", new Destination() { Address = "https://example.com" } } } } }; services.AddReverseProxy() - .LoadFromMemory(routes, clusters) - .AddTransformFactory() - .AddTransforms() - .AddTransforms(transformBuilderContext => - { - // For each route+cluster pair decide if we want to add transforms, and if so, which? - // This logic is re-run each time a route is rebuilt. - - transformBuilderContext.AddPathPrefix("/prefix"); - - // Only do this for routes that require auth. - if (string.Equals("token", transformBuilderContext.Route.AuthorizationPolicy)) - { - transformBuilderContext.AddRequestTransform(async transformContext => - { - // AuthN and AuthZ will have already been completed after request routing. - var ticket = await transformContext.HttpContext.AuthenticateAsync("token"); - var tokenService = transformContext.HttpContext.RequestServices.GetRequiredService(); - var token = await tokenService.GetAuthTokenAsync(ticket.Principal); - transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); - }); - } - }); - - services.AddHttpContextAccessor(); - services.AddSingleton(); - services.AddScoped(); - services.AddProxyTelemetryListener(); + // See InMemoryConfigProvider.cs + .LoadFromMemory(routes, clusters); } /// @@ -87,18 +58,17 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app) { app.UseRouting(); - app.UseAuthorization(); app.UseEndpoints(endpoints => { - endpoints.MapControllers(); endpoints.MapReverseProxy(proxyPipeline => { - // Custom endpoint selection + // Custom proxy middleware proxyPipeline.Use((context, next) => { var someCriteria = false; // MeetsCriteria(context); if (someCriteria) { + // Here we check available destinations for the current cluster and pick one using custom criteria. var availableDestinationsFeature = context.Features.Get(); var destination = availableDestinationsFeature.AvailableDestinations[0]; // PickDestination(availableDestinationsFeature.Destinations); // Load balancing will no-op if we've already reduced the list of available destinations to 1. @@ -107,9 +77,9 @@ public void Configure(IApplicationBuilder app) return next(); }); + // Don't forget to include these two middleware when you make a custom proxy pipeline (if you need them). proxyPipeline.UseAffinitizedDestinationLookup(); proxyPipeline.UseProxyLoadBalancing(); - proxyPipeline.UseRequestAffinitizer(); }); }); } diff --git a/samples/ReverseProxy.Config.Sample/CustomConfigFilter.cs b/samples/ReverseProxy.Config.Sample/CustomConfigFilter.cs index ca991bc10..3e605f994 100644 --- a/samples/ReverseProxy.Config.Sample/CustomConfigFilter.cs +++ b/samples/ReverseProxy.Config.Sample/CustomConfigFilter.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; using Microsoft.ReverseProxy.Abstractions; using Microsoft.ReverseProxy.Service; @@ -14,48 +12,12 @@ public class CustomConfigFilter : IProxyConfigFilter { public ValueTask ConfigureClusterAsync(Cluster cluster, CancellationToken cancel) { - // How to use custom metadata to configure clusters - if (cluster.Metadata?.TryGetValue("CustomHealth", out var customHealth) ?? false - && string.Equals(customHealth, "true", StringComparison.OrdinalIgnoreCase)) - { - cluster = cluster with - { - HealthCheck = new HealthCheckOptions - { - Active = new ActiveHealthCheckOptions - { - Enabled = true, - Policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures, - }, - Passive = cluster.HealthCheck?.Passive, - } - }; - } - - // Or wrap the meatadata in config sugar - var config = new ConfigurationBuilder().AddInMemoryCollection(cluster.Metadata).Build(); - if (config.GetValue("CustomHealth")) - { - cluster = cluster with - { - HealthCheck = new HealthCheckOptions - { - Active = new ActiveHealthCheckOptions - { - Enabled = true, - Policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures, - }, - Passive = cluster.HealthCheck?.Passive, - } - }; - } - return new ValueTask(cluster); } public ValueTask ConfigureRouteAsync(ProxyRoute route, CancellationToken cancel) { - // Do not let config based routes take priority over code based routes. + // Example: do not let config based routes take priority over code based routes. // Lower numbers are higher priority. Code routes default to 0. if (route.Order.HasValue && route.Order.Value < 1) { diff --git a/samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj b/samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj index abaec5790..979994a5b 100644 --- a/samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj +++ b/samples/ReverseProxy.Config.Sample/ReverseProxy.Config.Sample.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/ReverseProxy.Config.Sample/Startup.cs b/samples/ReverseProxy.Config.Sample/Startup.cs index 99776fc58..702a09d4a 100644 --- a/samples/ReverseProxy.Config.Sample/Startup.cs +++ b/samples/ReverseProxy.Config.Sample/Startup.cs @@ -2,10 +2,8 @@ // Licensed under the MIT License. using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.ReverseProxy.Middleware; namespace Microsoft.ReverseProxy.Sample { @@ -41,32 +39,9 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app) { app.UseRouting(); - app.UseAuthorization(); app.UseEndpoints(endpoints => { - endpoints.MapControllers(); - endpoints.MapReverseProxy(proxyPipeline => - { - // Custom endpoint selection - proxyPipeline.Use((context, next) => - { - var someCriteria = false; // MeetsCriteria(context); - if (someCriteria) - { - var availableDestinationsFeature = context.Features.Get(); - var destination = availableDestinationsFeature.AvailableDestinations[0]; // PickDestination(availableDestinationsFeature.Destinations); - // Load balancing will no-op if we've already reduced the list of available destinations to 1. - availableDestinationsFeature.AvailableDestinations = destination; - } - - return next(); - }); - proxyPipeline.UseAffinitizedDestinationLookup(); - proxyPipeline.UseProxyLoadBalancing(); - proxyPipeline.UseRequestAffinitizer(); - proxyPipeline.UsePassiveHealthChecks(); - }) - .ConfigureEndpoints((builder, route) => builder.WithDisplayName($"ReverseProxy {route.RouteId}-{route.ClusterId}")); + endpoints.MapReverseProxy(); }); } } diff --git a/samples/ReverseProxy.Config.Sample/appsettings.json b/samples/ReverseProxy.Config.Sample/appsettings.json index 2a4338680..f0d3c60dc 100644 --- a/samples/ReverseProxy.Config.Sample/appsettings.json +++ b/samples/ReverseProxy.Config.Sample/appsettings.json @@ -10,60 +10,14 @@ "Kestrel": { "Endpoints": { "https": { - "url": "https://localhost:5001" + "Url": "https://localhost:5001" }, "http": { - "url": "http://localhost:5000" + "Url": "http://localhost:5000" } } }, "ReverseProxy": { - "Clusters": { - "cluster1": { - "LoadBalancingPolicy": "Random", - "SessionAffinity": { - "Enabled": "true", - "Mode": "Cookie" - }, - "HealthCheck": { - "Active": { - "Enabled": "true", - "Interval": "00:00:10", - "Timeout": "00:00:10", - "Policy": "ConsecutiveFailures", - "Path": "/api/health" - }, - "Passive": { - "Enabled": "true", - "Policy": "TransportFailureRate", - "ReactivationPeriod": "00:05:00" - } - }, - "Metadata": { - "ConsecutiveFailuresHealthPolicy.Threshold": "3", - "TransportFailureRateHealthPolicy.RateLimit": "0.5" - }, - "Destinations": { - "cluster1/destination1": { - "Address": "https://localhost:10000/" - }, - "cluster1/destination2": { - "Address": "http://localhost:10010/" - } - } - }, - "cluster2": { - "Metadata": { - "CustomHealth": true - }, - "Destinations": { - "cluster2/destination1": { - "Address": "https://localhost:10001/", - "Health": "https://localhost:10001/api/health" - } - } - } - }, "Routes": [ { "RouteId": "route1", @@ -71,60 +25,57 @@ "Match": { "Methods": [ "GET", "POST" ], "Hosts": [ "localhost" ], - "Path": "/api/{action}" + "Path": "/api/{**catch-all}" } }, { "RouteId": "route2", "ClusterId": "cluster2", "Match": { - "Hosts": [ "localhost" ], - "Path": "/api/{plugin}/stuff/{*remainder}" + "Path": "{**catch-all}" }, "Transforms": [ - { "PathPattern": "/foo/{plugin}/bar/{remainder}" }, - { - "X-Forwarded": "proto,host,for,pathbase", - "Append": "true", - "Prefix": "X-Forwarded-" - }, - { - "Forwarded": "by,host,for,proto", - "ByFormat": "Random", - "ForFormat": "IpAndPort" - }, - { "ClientCert": "X-Client-Cert" }, - - { "PathSet": "/apis" }, - { "PathPrefix": "/apis" }, - { "PathRemovePrefix": "/apis" }, - - { "RequestHeadersCopy": "true" }, - { "RequestHeaderOriginalHost": "true" }, { "RequestHeader": "foo0", "Append": "bar" }, - { - "RequestHeader": "foo1", - "Set": "bar, baz" - }, - { - "RequestHeader": "clearMe", - "Set": "" - }, { "ResponseHeader": "foo", "Append": "bar", "When": "Always" - }, - { - "ResponseTrailer": "foo", - "Append": "trailer", - "When": "Always" } ] } - ] + ], + "Clusters": { + "cluster1": { + "LoadBalancingPolicy": "Random", + "SessionAffinity": { + "Enabled": "true" + }, + "HealthCheck": { + "Passive": { + "Enabled": "true", + "Policy": "TransportFailureRate", + "ReactivationPeriod": "00:05:00" + } + }, + "Destinations": { + "cluster1/destination1": { + "Address": "https://contoso.com/" + }, + "cluster1/destination2": { + "Address": "https://bing.com/" + } + } + }, + "cluster2": { + "Destinations": { + "cluster2/destination1": { + "Address": "https://example.com/" + } + } + } + } } } diff --git a/samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj b/samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj index abaec5790..979994a5b 100644 --- a/samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj +++ b/samples/ReverseProxy.Direct.Sample/ReverseProxy.Direct.Sample.csproj @@ -7,7 +7,7 @@ - + diff --git a/samples/ReverseProxy.Direct.Sample/Startup.cs b/samples/ReverseProxy.Direct.Sample/Startup.cs index dd2957dda..987a75f0d 100644 --- a/samples/ReverseProxy.Direct.Sample/Startup.cs +++ b/samples/ReverseProxy.Direct.Sample/Startup.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.ReverseProxy.Service.Proxy; +using Microsoft.ReverseProxy.Service.RuntimeModel.Transforms; namespace Microsoft.ReverseProxy.Sample { @@ -22,7 +23,6 @@ public class Startup /// public void ConfigureServices(IServiceCollection services) { - services.AddControllers(); services.AddHttpProxy(); } @@ -39,18 +39,15 @@ public void Configure(IApplicationBuilder app, IHttpProxy httpProxy) UseCookies = false }); - // Copy all request headers except Host var transformer = new CustomTransformer(); // or HttpTransformer.Default; var requestOptions = new RequestProxyOptions { Timeout = TimeSpan.FromSeconds(100) }; app.UseRouting(); - app.UseAuthorization(); app.UseEndpoints(endpoints => { - endpoints.MapControllers(); endpoints.Map("/{**catch-all}", async httpContext => { - await httpProxy.ProxyAsync(httpContext, "https://localhost:10000/", httpClient, requestOptions, transformer); + await httpProxy.ProxyAsync(httpContext, "https://example.com", httpClient, requestOptions, transformer); var errorFeature = httpContext.Features.Get(); if (errorFeature != null) { @@ -65,9 +62,18 @@ private class CustomTransformer : HttpTransformer { public override async Task TransformRequestAsync(HttpContext httpContext, HttpRequestMessage proxyRequest, string destinationPrefix) { - // Copy headers normally and then remove the host. - // Use the destination host from proxyRequest.RequestUri instead. + // Copy all request headers await base.TransformRequestAsync(httpContext, proxyRequest, destinationPrefix); + + // Customize the query string: + var queryContext = new QueryTransformContext(httpContext.Request); + queryContext.Collection.Remove("param1"); + queryContext.Collection["area"] = "xx2"; + + // Assign the custom uri. Be careful about extra slashes when concatenating here. + proxyRequest.RequestUri = new Uri(destinationPrefix + httpContext.Request.Path + queryContext.QueryString); + + // Suppress the original request header, use the one from the destination Uri. proxyRequest.Headers.Host = null; } } diff --git a/samples/ReverseProxy.Metrics.Sample/Program.cs b/samples/ReverseProxy.Metrics.Sample/Program.cs new file mode 100644 index 000000000..7c41e9acd --- /dev/null +++ b/samples/ReverseProxy.Metrics.Sample/Program.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.ReverseProxy.Sample +{ + /// + /// Class that contains the entrypoint for the Reverse Proxy sample app. + /// + public class Program + { + /// + /// Entrypoint of the application. + /// + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/samples/ReverseProxy.Metrics.Sample/Properties/launchSettings.json b/samples/ReverseProxy.Metrics.Sample/Properties/launchSettings.json new file mode 100644 index 000000000..870f99002 --- /dev/null +++ b/samples/ReverseProxy.Metrics.Sample/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "https://localhost:44356/", + "sslPort": 44356 + } + }, + "profiles": { + "ReverseProxy.Sample": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ReverseProxy.Code.Sample/ProxyMetricsConsumer.cs b/samples/ReverseProxy.Metrics.Sample/ProxyMetricsConsumer.cs similarity index 100% rename from samples/ReverseProxy.Code.Sample/ProxyMetricsConsumer.cs rename to samples/ReverseProxy.Metrics.Sample/ProxyMetricsConsumer.cs diff --git a/samples/ReverseProxy.Code.Sample/ProxyTelemetryConsumer.cs b/samples/ReverseProxy.Metrics.Sample/ProxyTelemetryConsumer.cs similarity index 100% rename from samples/ReverseProxy.Code.Sample/ProxyTelemetryConsumer.cs rename to samples/ReverseProxy.Metrics.Sample/ProxyTelemetryConsumer.cs diff --git a/samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj b/samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj new file mode 100644 index 000000000..010894d40 --- /dev/null +++ b/samples/ReverseProxy.Metrics.Sample/ReverseProxy.Metrics.Sample.csproj @@ -0,0 +1,17 @@ + + + + net5.0;netcoreapp3.1 + Exe + Microsoft.ReverseProxy.Sample + + + + + + + + + + + diff --git a/samples/ReverseProxy.Metrics.Sample/Startup.cs b/samples/ReverseProxy.Metrics.Sample/Startup.cs new file mode 100644 index 000000000..0c0048f3f --- /dev/null +++ b/samples/ReverseProxy.Metrics.Sample/Startup.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.ReverseProxy.Telemetry.Consumption; + +namespace Microsoft.ReverseProxy.Sample +{ + /// + /// ASP .NET Core pipeline initialization. + /// + public class Startup + { + private readonly IConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + public Startup(IConfiguration configuration) + { + _configuration = configuration; + } + + /// + /// This method gets called by the runtime. Use this method to add services to the container. + /// + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + services.AddReverseProxy() + .LoadFromConfig(_configuration.GetSection("ReverseProxy")); + + services.AddHttpContextAccessor(); + services.AddSingleton(); + services.AddScoped(); + services.AddProxyTelemetryListener(); + } + + /// + /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapReverseProxy(); + }); + } + } +} diff --git a/samples/BenchmarkApp/appsettings.Development.json b/samples/ReverseProxy.Metrics.Sample/appsettings.Development.json similarity index 100% rename from samples/BenchmarkApp/appsettings.Development.json rename to samples/ReverseProxy.Metrics.Sample/appsettings.Development.json diff --git a/samples/ReverseProxy.Metrics.Sample/appsettings.json b/samples/ReverseProxy.Metrics.Sample/appsettings.json new file mode 100644 index 000000000..7dec5dd3f --- /dev/null +++ b/samples/ReverseProxy.Metrics.Sample/appsettings.json @@ -0,0 +1,40 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + // "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "http": { + "Url": "http://localhost:5000" + }, + "https": { + "Url": "https://localhost:5001" + } + } + }, + "ReverseProxy": { + "Routes": [ + { + "RouteId": "route1", + "ClusterId": "cluster1", + "Match": { + "Path": "{**catch-all}" + } + } + ], + "Clusters": { + "cluster1": { + "Destinations": { + "cluster1/destination1": { + "Address": "https://example.com/" + } + } + } + } + } +} diff --git a/samples/ReverseProxy.ServiceFabric.Sample/ReverseProxy.ServiceFabric.Sample.csproj b/samples/ReverseProxy.ServiceFabric.Sample/ReverseProxy.ServiceFabric.Sample.csproj index db56df7d0..7395a4432 100644 --- a/samples/ReverseProxy.ServiceFabric.Sample/ReverseProxy.ServiceFabric.Sample.csproj +++ b/samples/ReverseProxy.ServiceFabric.Sample/ReverseProxy.ServiceFabric.Sample.csproj @@ -8,7 +8,6 @@ - diff --git a/samples/ReverseProxy.ServiceFabric.Sample/Startup.cs b/samples/ReverseProxy.ServiceFabric.Sample/Startup.cs index 6ee661555..008880318 100644 --- a/samples/ReverseProxy.ServiceFabric.Sample/Startup.cs +++ b/samples/ReverseProxy.ServiceFabric.Sample/Startup.cs @@ -38,14 +38,7 @@ public void ConfigureServices(IServiceCollection services) /// public void Configure(IApplicationBuilder app) { - app.Use((context, next) => - { - context.Response.Headers.Add("x-yarp-sf", Environment.MachineName); - return next(); - }); - app.UseRouting(); - app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/samples/ReverseProxy.Transforms.Sample/MyTransformFactory.cs b/samples/ReverseProxy.Transforms.Sample/MyTransformFactory.cs new file mode 100644 index 000000000..0adfc426e --- /dev/null +++ b/samples/ReverseProxy.Transforms.Sample/MyTransformFactory.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.ReverseProxy.Abstractions.Config; + +namespace Microsoft.ReverseProxy.Sample +{ + internal class MyTransformFactory : ITransformFactory + { + public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) + { + if (transformValues.TryGetValue("CustomTransform", out var value)) + { + if (string.IsNullOrEmpty(value)) + { + context.Errors.Add(new ArgumentException("A non-empty CustomTransform value is required")); + } + + return true; // Matched + } + return false; + } + + public bool Build(TransformBuilderContext context, IReadOnlyDictionary transformValues) + { + if (transformValues.TryGetValue("CustomTransform", out var value)) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("A non-empty CustomTransform value is required"); + } + + context.AddRequestTransform(transformContext => + { +#if NET + transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey("CustomTransform"), value); +#else + transformContext.ProxyRequest.Properties["CustomTransform"] = value; +#endif + return default; + }); + + return true; + } + + return false; + } + } +} diff --git a/samples/ReverseProxy.Code.Sample/MyTransformProvider.cs b/samples/ReverseProxy.Transforms.Sample/MyTransformProvider.cs similarity index 63% rename from samples/ReverseProxy.Code.Sample/MyTransformProvider.cs rename to samples/ReverseProxy.Transforms.Sample/MyTransformProvider.cs index 6d124008d..5d825e7bb 100644 --- a/samples/ReverseProxy.Code.Sample/MyTransformProvider.cs +++ b/samples/ReverseProxy.Transforms.Sample/MyTransformProvider.cs @@ -3,14 +3,13 @@ using System; using System.Net.Http; -using System.Threading.Tasks; using Microsoft.ReverseProxy.Abstractions.Config; namespace Microsoft.ReverseProxy.Sample { internal class MyTransformProvider : ITransformProvider { - public void Validate(TransformValidationContext context) + public void ValidateRoute(TransformRouteValidationContext context) { // Check all routes for a custom property and validate the associated transform data. string value = null; @@ -23,11 +22,25 @@ public void Validate(TransformValidationContext context) } } + public void ValidateCluster(TransformClusterValidationContext context) + { + // Check all clusters for a custom property and validate the associated transform data. + string value = null; + if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out value) ?? false) + { + if (string.IsNullOrEmpty(value)) + { + context.Errors.Add(new ArgumentException("A non-empty CustomMetadata value is required")); + } + } + } + public void Apply(TransformBuilderContext transformBuildContext) { // Check all routes for a custom property and add the associated transform. string value = null; - if (transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata", out value) ?? false) + if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata", out value) ?? false) + || (transformBuildContext.Cluster?.Metadata?.TryGetValue("CustomMetadata", out value) ?? false)) { if (string.IsNullOrEmpty(value)) { @@ -41,7 +54,7 @@ public void Apply(TransformBuilderContext transformBuildContext) #else transformContext.ProxyRequest.Properties["CustomMetadata"] = value; #endif - return Task.CompletedTask; + return default; }); } } diff --git a/samples/ReverseProxy.Transforms.Sample/Program.cs b/samples/ReverseProxy.Transforms.Sample/Program.cs new file mode 100644 index 000000000..7c41e9acd --- /dev/null +++ b/samples/ReverseProxy.Transforms.Sample/Program.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.ReverseProxy.Sample +{ + /// + /// Class that contains the entrypoint for the Reverse Proxy sample app. + /// + public class Program + { + /// + /// Entrypoint of the application. + /// + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/samples/ReverseProxy.Transforms.Sample/Properties/launchSettings.json b/samples/ReverseProxy.Transforms.Sample/Properties/launchSettings.json new file mode 100644 index 000000000..870f99002 --- /dev/null +++ b/samples/ReverseProxy.Transforms.Sample/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "https://localhost:44356/", + "sslPort": 44356 + } + }, + "profiles": { + "ReverseProxy.Sample": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj b/samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj new file mode 100644 index 000000000..979994a5b --- /dev/null +++ b/samples/ReverseProxy.Transforms.Sample/ReverseProxy.Transforms.Sample.csproj @@ -0,0 +1,17 @@ + + + + net5.0;netcoreapp3.1 + Exe + Microsoft.ReverseProxy.Sample + + + + + + + + + + + diff --git a/samples/ReverseProxy.Transforms.Sample/Startup.cs b/samples/ReverseProxy.Transforms.Sample/Startup.cs new file mode 100644 index 000000000..9336d8a76 --- /dev/null +++ b/samples/ReverseProxy.Transforms.Sample/Startup.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net.Http.Headers; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.ReverseProxy.Abstractions.Config; + +namespace Microsoft.ReverseProxy.Sample +{ + /// + /// ASP .NET Core pipeline initialization. + /// + public class Startup + { + private readonly IConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + public Startup(IConfiguration configuration) + { + _configuration = configuration; + } + + /// + /// This method gets called by the runtime. Use this method to add services to the container. + /// + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddReverseProxy() + .LoadFromConfig(_configuration.GetSection("ReverseProxy")) + .AddTransforms() // Adds custom transforms via code. + .AddTransformFactory() // Adds custom transforms via config. + // Add transforms inline + .AddTransforms(transformBuilderContext => + { + // For each route+cluster pair decide if we want to add transforms, and if so, which? + // This logic is re-run each time a route is rebuilt. + + transformBuilderContext.AddPathPrefix("/prefix"); + + // Only do this for routes that require auth. + if (string.Equals("token", transformBuilderContext.Route.AuthorizationPolicy)) + { + transformBuilderContext.AddRequestTransform(async transformContext => + { + // AuthN and AuthZ will have already been completed after request routing. + var ticket = await transformContext.HttpContext.AuthenticateAsync("token"); + var tokenService = transformContext.HttpContext.RequestServices.GetRequiredService(); + var token = await tokenService.GetAuthTokenAsync(ticket.Principal); + transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + }); + } + }); ; + } + + /// + /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapReverseProxy(); + }); + } + } +} diff --git a/samples/ReverseProxy.Code.Sample/TokenService.cs b/samples/ReverseProxy.Transforms.Sample/TokenService.cs similarity index 100% rename from samples/ReverseProxy.Code.Sample/TokenService.cs rename to samples/ReverseProxy.Transforms.Sample/TokenService.cs diff --git a/samples/ReverseProxy.Transforms.Sample/appsettings.Development.json b/samples/ReverseProxy.Transforms.Sample/appsettings.Development.json new file mode 100644 index 000000000..34b9d2085 --- /dev/null +++ b/samples/ReverseProxy.Transforms.Sample/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Debug", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/ReverseProxy.Transforms.Sample/appsettings.json b/samples/ReverseProxy.Transforms.Sample/appsettings.json new file mode 100644 index 000000000..6007fd8cc --- /dev/null +++ b/samples/ReverseProxy.Transforms.Sample/appsettings.json @@ -0,0 +1,73 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "https": { + "Url": "https://localhost:5001" + }, + "http": { + "Url": "http://localhost:5000" + } + } + }, + "ReverseProxy": { + "Routes": [ + { + "RouteId": "route1", + "ClusterId": "cluster1", + "Match": { + "Path": "{**catch-all}" + }, + "Transforms": [ + { "PathPrefix": "/prefix" }, + { "RequestHeadersCopy": "true" }, + { "RequestHeaderOriginalHost": "false" }, + { + "RequestHeader": "foo0", + "Append": "bar" + }, + { + "RequestHeader": "foo1", + "Set": "bar, baz" + }, + { + "RequestHeader": "clearMe", + "Set": "" + }, + { + "ResponseHeader": "foo", + "Append": "bar", + "When": "Always" + }, + { + "ResponseTrailer": "foo", + "Append": "trailer", + "When": "Always" + }, + { + "CustomTransform": "custom value" + } + ] + } + ], + "Clusters": { + "cluster1": { + "Metadata": { + "CustomMetadata": "custom value" + }, + "Destinations": { + "cluster1/destination1": { + "Address": "https://example.com" + } + } + } + } + } +} diff --git a/samples/SampleServer/Controllers/HttpController.cs b/samples/SampleServer/Controllers/HttpController.cs index 575f662fd..35457227e 100644 --- a/samples/SampleServer/Controllers/HttpController.cs +++ b/samples/SampleServer/Controllers/HttpController.cs @@ -71,53 +71,5 @@ public void Headers([FromBody] Dictionary headers) Response.Headers.Add(key, value); } } - - /// - /// Returns a 200 response after milliseconds - /// and containing with bytes in the response body. - /// - [HttpGet] - [HttpPut] - [HttpPost] - [HttpPatch] - [Route("/api/stress")] - public async Task Stress([FromQuery] int delay, [FromQuery] int responseSize) - { - var bodyReader = Request.BodyReader; - if (bodyReader != null) - { - while (true) - { - var a = await Request.BodyReader.ReadAsync(); - if (a.IsCompleted) - { - break; - } - } - } - - if (delay > 0) - { - await Task.Delay(delay); - } - - var bodyWriter = Response.BodyWriter; - if (bodyWriter != null && responseSize > 0) - { - const int WriteBufferSize = 4096; - - var remaining = responseSize; - var buffer = new byte[WriteBufferSize]; - - while (remaining > 0) - { - buffer[0] = (byte)(remaining * 17); // Make the output not all zeros - var toWrite = Math.Min(buffer.Length, remaining); - await bodyWriter.WriteAsync(new ReadOnlyMemory(buffer, 0, toWrite), - HttpContext.RequestAborted); - remaining -= toWrite; - } - } - } } } diff --git a/samples/SampleServer/Startup.cs b/samples/SampleServer/Startup.cs index 8615286af..c1a2ad03d 100644 --- a/samples/SampleServer/Startup.cs +++ b/samples/SampleServer/Startup.cs @@ -26,10 +26,6 @@ public void ConfigureServices(IServiceCollection services) /// public void Configure(IApplicationBuilder app) { - // Disabling https redirection behind the proxy. Forwarders are not currently set up so we can't tell if the external connection used https. - // Nor do we know the correct port to redirect to. - // app.UseHttpsRedirection(); - app.UseWebSockets(); app.UseRouting(); diff --git a/src/ReverseProxy.ServiceFabric/ServiceDiscovery/ServiceExtensionLabelsProvider.cs b/src/ReverseProxy.ServiceFabric/ServiceDiscovery/ServiceExtensionLabelsProvider.cs index 8a20e9c81..28c8f11d7 100644 --- a/src/ReverseProxy.ServiceFabric/ServiceDiscovery/ServiceExtensionLabelsProvider.cs +++ b/src/ReverseProxy.ServiceFabric/ServiceDiscovery/ServiceExtensionLabelsProvider.cs @@ -186,7 +186,7 @@ private void ApplyAppParamReplacements(Dictionary labels, Applic /// /// Gets the labels from the extensions of the provided raw service manifest. /// - private Dictionary ExtractLabels( + private static Dictionary ExtractLabels( string rawServiceManifest, string targetServiceTypeName) { diff --git a/src/ReverseProxy.TelemetryConsumption/Microsoft.ReverseProxy.Telemetry.Consumption.csproj b/src/ReverseProxy.TelemetryConsumption/Microsoft.ReverseProxy.Telemetry.Consumption.csproj index 3b56452d9..7fa1ef8c8 100644 --- a/src/ReverseProxy.TelemetryConsumption/Microsoft.ReverseProxy.Telemetry.Consumption.csproj +++ b/src/ReverseProxy.TelemetryConsumption/Microsoft.ReverseProxy.Telemetry.Consumption.csproj @@ -1,7 +1,7 @@ - Microsoft.ReverseProxy extension package for in-process telemtry consumption + Microsoft.ReverseProxy extension package for in-process telemetry consumption net5.0;netcoreapp3.1 Library Microsoft.ReverseProxy.Telemetry.Consumption diff --git a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ActivityContextHeaders.cs b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ActivityContextHeaders.cs new file mode 100644 index 000000000..202e674e5 --- /dev/null +++ b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ActivityContextHeaders.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.ReverseProxy.Abstractions +{ + [Flags] + public enum ActivityContextHeaders + { + None = 0, + Baggage = 1, + CorrelationContext = 2, + BaggageAndCorrelationContext = Baggage | CorrelationContext + } +} diff --git a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptions.cs b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptions.cs index 4d83d61fb..ada84c572 100644 --- a/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptions.cs +++ b/src/ReverseProxy/Abstractions/ClusterDiscovery/Contract/ProxyHttpClientOptions.cs @@ -40,14 +40,18 @@ public sealed record ProxyHttpClientOptions public int? MaxConnectionsPerServer { get; init; } /// - /// Enables or disables the activity correlation headers for outgoing requests. + /// Specifies the activity correlation headers for outgoing requests. /// - public bool? PropagateActivityContext { get; init; } - - // TODO: Add this property once we have migrated to SDK version that supports it. - //public bool? EnableMultipleHttp2Connections { get; init; } + public ActivityContextHeaders? ActivityContextHeaders { get; init; } #if NET + /// + /// Gets or sets a value that indicates whether additional HTTP/2 connections can + // be established to the same server when the maximum number of concurrent streams + // is reached on all existing connections. + /// + public bool? EnableMultipleHttp2Connections { get; init; } + /// /// Enables non-ASCII header encoding for outgoing requests. /// @@ -66,12 +70,12 @@ public bool Equals(ProxyHttpClientOptions other) && CertEquals(ClientCertificate, other.ClientCertificate) && DangerousAcceptAnyServerCertificate == other.DangerousAcceptAnyServerCertificate && MaxConnectionsPerServer == other.MaxConnectionsPerServer - && PropagateActivityContext == other.PropagateActivityContext #if NET + && EnableMultipleHttp2Connections == other.EnableMultipleHttp2Connections // Comparing by reference is fine here since Encoding.GetEncoding returns the same instance for each encoding. && RequestHeaderEncoding == other.RequestHeaderEncoding #endif - ; + && ActivityContextHeaders == other.ActivityContextHeaders; } private static bool CertEquals(X509Certificate2 certificate1, X509Certificate2 certificate2) @@ -96,11 +100,11 @@ public override int GetHashCode() ClientCertificate?.Thumbprint, DangerousAcceptAnyServerCertificate, MaxConnectionsPerServer, - PropagateActivityContext #if NET - , RequestHeaderEncoding + EnableMultipleHttp2Connections, + RequestHeaderEncoding, #endif - ); + ActivityContextHeaders); } } } diff --git a/src/ReverseProxy/Abstractions/Config/ITransformBuilder.cs b/src/ReverseProxy/Abstractions/Config/ITransformBuilder.cs index a7f7b2aee..f9819f636 100644 --- a/src/ReverseProxy/Abstractions/Config/ITransformBuilder.cs +++ b/src/ReverseProxy/Abstractions/Config/ITransformBuilder.cs @@ -17,11 +17,16 @@ public interface ITransformBuilder /// Validates that each transform for the given route is known and has the expected parameters. All transforms are validated /// so all errors can be reported. /// - IReadOnlyList Validate(ProxyRoute route); + IReadOnlyList ValidateRoute(ProxyRoute route); + + /// + /// Validates that any cluster data needed for transforms is valid. + /// + IReadOnlyList ValidateCluster(Cluster cluster); /// /// Builds the transforms for the given route into executable rules. /// - HttpTransformer Build(ProxyRoute route); + HttpTransformer Build(ProxyRoute route, Cluster cluster); } } diff --git a/src/ReverseProxy/Abstractions/Config/ITransformFactory.cs b/src/ReverseProxy/Abstractions/Config/ITransformFactory.cs index 44935588f..c202c7aa3 100644 --- a/src/ReverseProxy/Abstractions/Config/ITransformFactory.cs +++ b/src/ReverseProxy/Abstractions/Config/ITransformFactory.cs @@ -16,7 +16,7 @@ public interface ITransformFactory /// The context to add any generated errors to. /// The transform values to validate. /// True if this factory matches the given transform, otherwise false. - bool Validate(TransformValidationContext context, IReadOnlyDictionary transformValues); + bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues); /// /// Checks if the given transform values match a known transform, and if so, generates a transform and diff --git a/src/ReverseProxy/Abstractions/Config/ITransformProvider.cs b/src/ReverseProxy/Abstractions/Config/ITransformProvider.cs index 5348d6d96..e2bfa298a 100644 --- a/src/ReverseProxy/Abstractions/Config/ITransformProvider.cs +++ b/src/ReverseProxy/Abstractions/Config/ITransformProvider.cs @@ -12,7 +12,13 @@ public interface ITransformProvider /// Validates any route data needed for transforms. /// /// The context to add any generated errors to. - void Validate(TransformValidationContext context); + void ValidateRoute(TransformRouteValidationContext context); + + /// + /// Validates any cluster data needed for transforms. + /// + /// The context to add any generated errors to. + void ValidateCluster(TransformClusterValidationContext context); /// /// Inspect the given route and conditionally add transforms. diff --git a/src/ReverseProxy/Abstractions/Config/TransformBuilderContext.cs b/src/ReverseProxy/Abstractions/Config/TransformBuilderContext.cs index f0947a259..12e776bc8 100644 --- a/src/ReverseProxy/Abstractions/Config/TransformBuilderContext.cs +++ b/src/ReverseProxy/Abstractions/Config/TransformBuilderContext.cs @@ -22,6 +22,11 @@ public class TransformBuilderContext /// public ProxyRoute Route { get; init; } + /// + /// The cluster config the route is associated with. + /// + public Cluster Cluster { get; init; } + /// /// Indicates if request headers should all be copied to the proxy request before transforms are applied. /// diff --git a/src/ReverseProxy/Abstractions/Config/TransformBuilderContextFuncExtensions.cs b/src/ReverseProxy/Abstractions/Config/TransformBuilderContextFuncExtensions.cs index 8773431a7..d8d368f16 100644 --- a/src/ReverseProxy/Abstractions/Config/TransformBuilderContextFuncExtensions.cs +++ b/src/ReverseProxy/Abstractions/Config/TransformBuilderContextFuncExtensions.cs @@ -16,7 +16,7 @@ public static class TransformBuilderContextFuncExtensions /// /// Adds a transform Func that runs on each request for the given route. /// - public static TransformBuilderContext AddRequestTransform(this TransformBuilderContext context, Func func) + public static TransformBuilderContext AddRequestTransform(this TransformBuilderContext context, Func func) { if (context is null) { @@ -35,7 +35,7 @@ public static TransformBuilderContext AddRequestTransform(this TransformBuilderC /// /// Adds a transform Func that runs on each response for the given route. /// - public static TransformBuilderContext AddResponseTransform(this TransformBuilderContext context, Func func) + public static TransformBuilderContext AddResponseTransform(this TransformBuilderContext context, Func func) { if (context is null) { @@ -54,7 +54,7 @@ public static TransformBuilderContext AddResponseTransform(this TransformBuilder /// /// Adds a transform Func that runs on each response for the given route. /// - public static TransformBuilderContext AddResponseTrailersTransform(this TransformBuilderContext context, Func func) + public static TransformBuilderContext AddResponseTrailersTransform(this TransformBuilderContext context, Func func) { if (context is null) { diff --git a/src/ReverseProxy/Abstractions/Config/TransformClusterValidationContext.cs b/src/ReverseProxy/Abstractions/Config/TransformClusterValidationContext.cs new file mode 100644 index 000000000..6dc223c6a --- /dev/null +++ b/src/ReverseProxy/Abstractions/Config/TransformClusterValidationContext.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace Microsoft.ReverseProxy.Abstractions.Config +{ + /// + /// State used when validating transforms for the given cluster. + /// + public class TransformClusterValidationContext + { + /// + /// Application services that can be used to validate transforms. + /// + public IServiceProvider Services { get; init; } + + /// + /// The cluster configuration that may be used when creating transforms. + /// + public Cluster Cluster { get; init; } + + /// + /// The accumulated list of validation errors for this cluster. + /// Add validation errors here. + /// + public IList Errors { get; } = new List(); + } +} diff --git a/src/ReverseProxy/Abstractions/Config/TransformValidationContext.cs b/src/ReverseProxy/Abstractions/Config/TransformRouteValidationContext.cs similarity index 94% rename from src/ReverseProxy/Abstractions/Config/TransformValidationContext.cs rename to src/ReverseProxy/Abstractions/Config/TransformRouteValidationContext.cs index 2eaddaeb6..7927306a1 100644 --- a/src/ReverseProxy/Abstractions/Config/TransformValidationContext.cs +++ b/src/ReverseProxy/Abstractions/Config/TransformRouteValidationContext.cs @@ -9,7 +9,7 @@ namespace Microsoft.ReverseProxy.Abstractions.Config /// /// State used when validating transforms for the given route. /// - public class TransformValidationContext + public class TransformRouteValidationContext { /// /// Application services that can be used to validate transforms. diff --git a/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs b/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs index 68fd75bcc..71b0444b1 100644 --- a/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs +++ b/src/ReverseProxy/Configuration/ConfigurationConfigProvider.cs @@ -326,10 +326,11 @@ private ProxyHttpClientOptions CreateProxyHttpClientOptions(IConfigurationSectio DangerousAcceptAnyServerCertificate = section.ReadBool(nameof(ProxyHttpClientOptions.DangerousAcceptAnyServerCertificate)), ClientCertificate = clientCertificate, MaxConnectionsPerServer = section.ReadInt32(nameof(ProxyHttpClientOptions.MaxConnectionsPerServer)), - PropagateActivityContext = section.ReadBool(nameof(ProxyHttpClientOptions.PropagateActivityContext)), #if NET - RequestHeaderEncoding = section[nameof(ProxyHttpClientOptions.RequestHeaderEncoding)] is string encoding ? Encoding.GetEncoding(encoding) : null + EnableMultipleHttp2Connections = section.ReadBool(nameof(ProxyHttpClientOptions.EnableMultipleHttp2Connections)), + RequestHeaderEncoding = section[nameof(ProxyHttpClientOptions.RequestHeaderEncoding)] is string encoding ? Encoding.GetEncoding(encoding) : null, #endif + ActivityContextHeaders = section.ReadEnum(nameof(ProxyHttpClientOptions.ActivityContextHeaders)) }; } diff --git a/src/ReverseProxy/Configuration/DependencyInjection/BuilderExtensions/IReverseProxyBuilderExtensions.cs b/src/ReverseProxy/Configuration/DependencyInjection/BuilderExtensions/IReverseProxyBuilderExtensions.cs index 14aa6b61f..973ad55f0 100644 --- a/src/ReverseProxy/Configuration/DependencyInjection/BuilderExtensions/IReverseProxyBuilderExtensions.cs +++ b/src/ReverseProxy/Configuration/DependencyInjection/BuilderExtensions/IReverseProxyBuilderExtensions.cs @@ -84,6 +84,7 @@ public static IReverseProxyBuilder AddSessionAffinityProvider(this IReverseProxy new ServiceDescriptor(typeof(ISessionAffinityProvider), typeof(CookieSessionAffinityProvider), ServiceLifetime.Singleton), new ServiceDescriptor(typeof(ISessionAffinityProvider), typeof(CustomHeaderSessionAffinityProvider), ServiceLifetime.Singleton) }); + builder.AddTransforms(); return builder; } diff --git a/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs b/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs index d283d3564..5de60448f 100644 --- a/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs +++ b/src/ReverseProxy/Configuration/ReverseProxyIEndpointRouteBuilderExtensions.cs @@ -26,7 +26,6 @@ public static void MapReverseProxy(this IEndpointRouteBuilder endpoints) { app.UseAffinitizedDestinationLookup(); app.UseProxyLoadBalancing(); - app.UseRequestAffinitizer(); app.UsePassiveHealthChecks(); }); } diff --git a/src/ReverseProxy/Middleware/AffinitizeRequestMiddleware.cs b/src/ReverseProxy/Middleware/AffinitizeRequestMiddleware.cs deleted file mode 100644 index ecd0d50d3..000000000 --- a/src/ReverseProxy/Middleware/AffinitizeRequestMiddleware.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.ReverseProxy.Abstractions; -using Microsoft.ReverseProxy.Abstractions.ClusterDiscovery.Contract; -using Microsoft.ReverseProxy.RuntimeModel; -using Microsoft.ReverseProxy.Service.SessionAffinity; -using Microsoft.ReverseProxy.Utilities; - -namespace Microsoft.ReverseProxy.Middleware -{ - /// - /// Affinitizes request to a chosen . - /// - internal class AffinitizeRequestMiddleware - { - private readonly Random _random = new Random(); - private readonly RequestDelegate _next; - private readonly IDictionary _sessionAffinityProviders; - private readonly ILogger _logger; - - public AffinitizeRequestMiddleware( - RequestDelegate next, - IEnumerable sessionAffinityProviders, - ILogger logger) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _sessionAffinityProviders = sessionAffinityProviders?.ToDictionaryByUniqueId(p => p.Mode) ?? throw new ArgumentNullException(nameof(sessionAffinityProviders)); - } - - public Task Invoke(HttpContext context) - { - var proxyFeature = context.GetRequiredProxyFeature(); - var options = proxyFeature.ClusterConfig.Options.SessionAffinity; - - if ((options?.Enabled).GetValueOrDefault()) - { - var candidateDestinations = proxyFeature.AvailableDestinations; - - if (candidateDestinations.Count == 0) - { - var cluster = context.GetRequiredCluster(); - // Only log the warning about missing destinations here, but allow the request to proceed further. - // The final check for selected destination is to be done at the pipeline end. - Log.NoDestinationOnClusterToEstablishRequestAffinity(_logger, cluster.ClusterId); - } - else - { - var chosenDestination = candidateDestinations[0]; - if (candidateDestinations.Count > 1) - { - var cluster = context.GetRequiredCluster(); - Log.MultipleDestinationsOnClusterToEstablishRequestAffinity(_logger, cluster.ClusterId); - // It's assumed that all of them match to the request's affinity key. - chosenDestination = candidateDestinations[_random.Next(candidateDestinations.Count)]; - proxyFeature.AvailableDestinations = chosenDestination; - } - - AffinitizeRequest(context, options, chosenDestination); - } - } - - return _next(context); - } - - private void AffinitizeRequest(HttpContext context, SessionAffinityOptions options, DestinationInfo destination) - { - var currentProvider = _sessionAffinityProviders.GetRequiredServiceById(options.Mode, SessionAffinityConstants.Modes.Cookie); - currentProvider.AffinitizeRequest(context, options, destination); - } - - private static class Log - { - private static readonly Action _multipleDestinationsOnClusterToEstablishRequestAffinity = LoggerMessage.Define( - LogLevel.Warning, - EventIds.MultipleDestinationsOnClusterToEstablishRequestAffinity, - "The request still has multiple destinations on the cluster '{clusterId}' to choose from when establishing affinity, load balancing may not be properly configured. A random destination will be used."); - - private static readonly Action _noDestinationOnClusterToEstablishRequestAffinity = LoggerMessage.Define( - LogLevel.Warning, - EventIds.NoDestinationOnClusterToEstablishRequestAffinity, - "The request doesn't have any destinations on the cluster '{clusterId}' to choose from when establishing affinity, load balancing may not be properly configured."); - - public static void MultipleDestinationsOnClusterToEstablishRequestAffinity(ILogger logger, string clusterId) - { - _multipleDestinationsOnClusterToEstablishRequestAffinity(logger, clusterId, null); - } - - public static void NoDestinationOnClusterToEstablishRequestAffinity(ILogger logger, string clusterId) - { - _noDestinationOnClusterToEstablishRequestAffinity(logger, clusterId, null); - } - } - } -} diff --git a/src/ReverseProxy/Middleware/ProxyMiddlewareAppBuilderExtensions.cs b/src/ReverseProxy/Middleware/ProxyMiddlewareAppBuilderExtensions.cs index ebc4de557..0d9b9ad37 100644 --- a/src/ReverseProxy/Middleware/ProxyMiddlewareAppBuilderExtensions.cs +++ b/src/ReverseProxy/Middleware/ProxyMiddlewareAppBuilderExtensions.cs @@ -22,23 +22,13 @@ public static IApplicationBuilder UseProxyLoadBalancing(this IApplicationBuilder /// Checks if a request has an established affinity relationship and if the associated destination is available. /// This should be placed before load balancing and other destination selection components. /// Requests without an affinity relationship will be processed normally and have the affinity relationship - /// established by a later component. See . + /// established by a later component. /// public static IApplicationBuilder UseAffinitizedDestinationLookup(this IApplicationBuilder builder) { return builder.UseMiddleware(); } - /// - /// Establishes the affinity relationship to the selected destination. - /// If there are multiple affinitized destinations found for the request, it randomly picks one of them. - /// This should be placed after load balancing and other destination selection processes. - /// - public static IApplicationBuilder UseRequestAffinitizer(this IApplicationBuilder builder) - { - return builder.UseMiddleware(); - } - /// /// Passively checks destinations health by watching for successes and failures in client request proxying. /// diff --git a/src/ReverseProxy/Service/Config/ActionTransformProvider.cs b/src/ReverseProxy/Service/Config/ActionTransformProvider.cs index 44582ff52..f15936d47 100644 --- a/src/ReverseProxy/Service/Config/ActionTransformProvider.cs +++ b/src/ReverseProxy/Service/Config/ActionTransformProvider.cs @@ -20,7 +20,11 @@ public void Apply(TransformBuilderContext transformBuildContext) _action(transformBuildContext); } - public void Validate(TransformValidationContext context) + public void ValidateRoute(TransformRouteValidationContext context) + { + } + + public void ValidateCluster(TransformClusterValidationContext context) { } } diff --git a/src/ReverseProxy/Service/Config/ConfigValidator.cs b/src/ReverseProxy/Service/Config/ConfigValidator.cs index ee6f29eab..62e017462 100644 --- a/src/ReverseProxy/Service/Config/ConfigValidator.cs +++ b/src/ReverseProxy/Service/Config/ConfigValidator.cs @@ -14,7 +14,6 @@ using Microsoft.ReverseProxy.Abstractions; using Microsoft.ReverseProxy.Abstractions.ClusterDiscovery.Contract; using Microsoft.ReverseProxy.Abstractions.RouteDiscovery.Contract; -using Microsoft.ReverseProxy.Service.Config; using Microsoft.ReverseProxy.Service.HealthChecks; using Microsoft.ReverseProxy.Service.LoadBalancing; using Microsoft.ReverseProxy.Service.SessionAffinity; @@ -58,7 +57,6 @@ internal class ConfigValidator : IConfigValidator private readonly IAuthorizationPolicyProvider _authorizationPolicyProvider; private readonly ICorsPolicyProvider _corsPolicyProvider; private readonly IDictionary _loadBalancingPolicies; - private readonly IDictionary _sessionAffinityProviders; private readonly IDictionary _affinityFailurePolicies; private readonly IDictionary _activeHealthCheckPolicies; private readonly IDictionary _passiveHealthCheckPolicies; @@ -68,7 +66,6 @@ public ConfigValidator(ITransformBuilder transformBuilder, IAuthorizationPolicyProvider authorizationPolicyProvider, ICorsPolicyProvider corsPolicyProvider, IEnumerable loadBalancingPolicies, - IEnumerable sessionAffinityProviders, IEnumerable affinityFailurePolicies, IEnumerable activeHealthCheckPolicies, IEnumerable passiveHealthCheckPolicies) @@ -77,7 +74,6 @@ public ConfigValidator(ITransformBuilder transformBuilder, _authorizationPolicyProvider = authorizationPolicyProvider ?? throw new ArgumentNullException(nameof(authorizationPolicyProvider)); _corsPolicyProvider = corsPolicyProvider ?? throw new ArgumentNullException(nameof(corsPolicyProvider)); _loadBalancingPolicies = loadBalancingPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(loadBalancingPolicies)); - _sessionAffinityProviders = sessionAffinityProviders?.ToDictionaryByUniqueId(p => p.Mode) ?? throw new ArgumentNullException(nameof(sessionAffinityProviders)); _affinityFailurePolicies = affinityFailurePolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(affinityFailurePolicies)); _activeHealthCheckPolicies = activeHealthCheckPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(activeHealthCheckPolicies)); _passiveHealthCheckPolicies = passiveHealthCheckPolicies?.ToDictionaryByUniqueId(p => p.Name) ?? throw new ArgumentNullException(nameof(passiveHealthCheckPolicies)); @@ -94,7 +90,7 @@ public async ValueTask> ValidateRouteAsync(ProxyRoute route) errors.Add(new ArgumentException("Missing Route Id.")); } - errors.AddRange(_transformBuilder.Validate(route)); + errors.AddRange(_transformBuilder.ValidateRoute(route)); await ValidateAuthorizationPolicyAsync(errors, route.AuthorizationPolicy, route.RouteId); await ValidateCorsPolicyAsync(errors, route.CorsPolicy, route.RouteId); @@ -128,6 +124,7 @@ public ValueTask> ValidateClusterAsync(Cluster cluster) errors.Add(new ArgumentException("Missing Cluster Id.")); } + errors.AddRange(_transformBuilder.ValidateCluster(cluster)); ValidateLoadBalancing(errors, cluster); ValidateSessionAffinity(errors, cluster); ValidateProxyHttpClient(errors, cluster); @@ -317,17 +314,7 @@ private void ValidateSessionAffinity(IList errors, Cluster cluster) return; } - var affinityMode = cluster.SessionAffinity.Mode; - if (string.IsNullOrEmpty(affinityMode)) - { - // The default. - affinityMode = SessionAffinityConstants.Modes.Cookie; - } - - if (!_sessionAffinityProviders.ContainsKey(affinityMode)) - { - errors.Add(new ArgumentException($"No matching {nameof(ISessionAffinityProvider)} found for the session affinity mode '{affinityMode}' set on the cluster '{cluster.Id}'.")); - } + // Note some affinity validation takes place in AffinitizeTransformProvider.ValidateCluster. var affinityFailurePolicy = cluster.SessionAffinity.FailurePolicy; if (string.IsNullOrEmpty(affinityFailurePolicy)) diff --git a/src/ReverseProxy/Service/Config/ForwardedTransformFactory.cs b/src/ReverseProxy/Service/Config/ForwardedTransformFactory.cs index 5e844fcc5..10c472163 100644 --- a/src/ReverseProxy/Service/Config/ForwardedTransformFactory.cs +++ b/src/ReverseProxy/Service/Config/ForwardedTransformFactory.cs @@ -31,7 +31,7 @@ public ForwardedTransformFactory(IRandomFactory randomFactory) _randomFactory = randomFactory ?? throw new ArgumentNullException(nameof(randomFactory)); } - public bool Validate(TransformValidationContext context, IReadOnlyDictionary transformValues) + public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(XForwardedKey, out var xforwardedHeaders)) { diff --git a/src/ReverseProxy/Service/Config/HttpMethodTransformFactory.cs b/src/ReverseProxy/Service/Config/HttpMethodTransformFactory.cs index e2f683813..bee75d9e6 100644 --- a/src/ReverseProxy/Service/Config/HttpMethodTransformFactory.cs +++ b/src/ReverseProxy/Service/Config/HttpMethodTransformFactory.cs @@ -12,7 +12,7 @@ internal class HttpMethodTransformFactory : ITransformFactory internal static readonly string HttpMethodChangeKey = "HttpMethodChange"; internal static readonly string SetKey = "Set"; - public bool Validate(TransformValidationContext context, IReadOnlyDictionary transformValues) + public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(HttpMethodChangeKey, out var _)) { diff --git a/src/ReverseProxy/Service/Config/PathTransformFactory.cs b/src/ReverseProxy/Service/Config/PathTransformFactory.cs index 00f597020..ce6b41cc0 100644 --- a/src/ReverseProxy/Service/Config/PathTransformFactory.cs +++ b/src/ReverseProxy/Service/Config/PathTransformFactory.cs @@ -24,7 +24,7 @@ public PathTransformFactory(TemplateBinderFactory binderFactory) _binderFactory = binderFactory ?? throw new ArgumentNullException(nameof(binderFactory)); } - public bool Validate(TransformValidationContext context, IReadOnlyDictionary transformValues) + public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(PathSetKey, out var pathSet)) { diff --git a/src/ReverseProxy/Service/Config/QueryTransformFactory.cs b/src/ReverseProxy/Service/Config/QueryTransformFactory.cs index 5a161fb4d..19fe046d8 100644 --- a/src/ReverseProxy/Service/Config/QueryTransformFactory.cs +++ b/src/ReverseProxy/Service/Config/QueryTransformFactory.cs @@ -15,7 +15,7 @@ internal class QueryTransformFactory : ITransformFactory internal static readonly string AppendKey = "Append"; internal static readonly string SetKey = "Set"; - public bool Validate(TransformValidationContext context, IReadOnlyDictionary transformValues) + public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(QueryValueParameterKey, out var queryValueParameter)) { diff --git a/src/ReverseProxy/Service/Config/RequestHeadersTransformFactory.cs b/src/ReverseProxy/Service/Config/RequestHeadersTransformFactory.cs index 7e80d864d..b0a1833bb 100644 --- a/src/ReverseProxy/Service/Config/RequestHeadersTransformFactory.cs +++ b/src/ReverseProxy/Service/Config/RequestHeadersTransformFactory.cs @@ -15,7 +15,7 @@ internal class RequestHeadersTransformFactory : ITransformFactory internal static readonly string AppendKey = "Append"; internal static readonly string SetKey = "Set"; - public bool Validate(TransformValidationContext context, IReadOnlyDictionary transformValues) + public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(RequestHeadersCopyKey, out var copyHeaders)) { diff --git a/src/ReverseProxy/Service/Config/ResponseTransformFactory.cs b/src/ReverseProxy/Service/Config/ResponseTransformFactory.cs index c34305268..7548997e5 100644 --- a/src/ReverseProxy/Service/Config/ResponseTransformFactory.cs +++ b/src/ReverseProxy/Service/Config/ResponseTransformFactory.cs @@ -19,7 +19,7 @@ internal class ResponseTransformFactory : ITransformFactory internal static readonly string AppendKey = "Append"; internal static readonly string SetKey = "Set"; - public bool Validate(TransformValidationContext context, IReadOnlyDictionary transformValues) + public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue(ResponseHeadersCopyKey, out var copyHeaders)) { diff --git a/src/ReverseProxy/Service/Config/TransformBuilder.cs b/src/ReverseProxy/Service/Config/TransformBuilder.cs index 10e812512..cb4f668c6 100644 --- a/src/ReverseProxy/Service/Config/TransformBuilder.cs +++ b/src/ReverseProxy/Service/Config/TransformBuilder.cs @@ -32,9 +32,9 @@ public TransformBuilder(IServiceProvider services, IEnumerable - public IReadOnlyList Validate(ProxyRoute route) + public IReadOnlyList ValidateRoute(ProxyRoute route) { - var context = new TransformValidationContext() + var context = new TransformRouteValidationContext() { Services = _services, Route = route, @@ -65,20 +65,40 @@ public IReadOnlyList Validate(ProxyRoute route) // Let the app add any more validation it wants. foreach (var transformProvider in _providers) { - transformProvider.Validate(context); + transformProvider.ValidateRoute(context); } - return context.Errors.ToList(); + // We promise not to modify the list after we return it. + return (IReadOnlyList)context.Errors; } /// - public HttpTransformer Build(ProxyRoute route) + public IReadOnlyList ValidateCluster(Cluster cluster) { - return BuildInternal(route); + var context = new TransformClusterValidationContext() + { + Services = _services, + Cluster = cluster, + }; + + // Let the app add any more validation it wants. + foreach (var transformProvider in _providers) + { + transformProvider.ValidateCluster(context); + } + + // We promise not to modify the list after we return it. + return (IReadOnlyList)context.Errors; + } + + /// + public HttpTransformer Build(ProxyRoute route, Cluster cluster) + { + return BuildInternal(route, cluster); } // This is separate from Build for testing purposes. - internal StructuredTransformer BuildInternal(ProxyRoute route) + internal StructuredTransformer BuildInternal(ProxyRoute route, Cluster cluster) { var rawTransforms = route.Transforms; @@ -86,6 +106,7 @@ internal StructuredTransformer BuildInternal(ProxyRoute route) { Services = _services, Route = route, + Cluster = cluster, }; if (rawTransforms?.Count > 0) diff --git a/src/ReverseProxy/Service/Config/TransformHelpers.cs b/src/ReverseProxy/Service/Config/TransformHelpers.cs index 2e665319f..872468925 100644 --- a/src/ReverseProxy/Service/Config/TransformHelpers.cs +++ b/src/ReverseProxy/Service/Config/TransformHelpers.cs @@ -9,7 +9,7 @@ namespace Microsoft.ReverseProxy.Service.Config { public static class TransformHelpers { - public static void TryCheckTooManyParameters(TransformValidationContext context, IReadOnlyDictionary rawTransform, int expected) + public static void TryCheckTooManyParameters(TransformRouteValidationContext context, IReadOnlyDictionary rawTransform, int expected) { if (rawTransform.Count > expected) { diff --git a/src/ReverseProxy/Service/HealthChecks/ConsecutiveFailuresHealthPolicy.cs b/src/ReverseProxy/Service/HealthChecks/ConsecutiveFailuresHealthPolicy.cs index 80dcac483..9f775bd98 100644 --- a/src/ReverseProxy/Service/HealthChecks/ConsecutiveFailuresHealthPolicy.cs +++ b/src/ReverseProxy/Service/HealthChecks/ConsecutiveFailuresHealthPolicy.cs @@ -56,7 +56,7 @@ private double GetFailureThreshold(ClusterInfo cluster) return thresholdEntry.GetParsedOrDefault(_options.DefaultThreshold); } - private DestinationHealth EvaluateHealthState(double threshold, HttpResponseMessage response, AtomicCounter count) + private static DestinationHealth EvaluateHealthState(double threshold, HttpResponseMessage response, AtomicCounter count) { DestinationHealth newHealth; if (response != null && response.IsSuccessStatusCode) diff --git a/src/ReverseProxy/Service/LoadBalancing/ILoadBalancingPolicy.cs b/src/ReverseProxy/Service/LoadBalancing/ILoadBalancingPolicy.cs index 938782625..724511fbc 100644 --- a/src/ReverseProxy/Service/LoadBalancing/ILoadBalancingPolicy.cs +++ b/src/ReverseProxy/Service/LoadBalancing/ILoadBalancingPolicy.cs @@ -16,7 +16,7 @@ public interface ILoadBalancingPolicy /// /// A unique identifier for this load balancing policy. This will be referenced from config. /// - public string Name { get; } + string Name { get; } /// /// Picks a destination to send traffic to. diff --git a/src/ReverseProxy/Service/Management/ProxyConfigManager.cs b/src/ReverseProxy/Service/Management/ProxyConfigManager.cs index a4cfd8970..c9e1301d2 100644 --- a/src/ReverseProxy/Service/Management/ProxyConfigManager.cs +++ b/src/ReverseProxy/Service/Management/ProxyConfigManager.cs @@ -471,14 +471,6 @@ private bool UpdateRuntimeRoutes(IList routes) var newConfig = BuildRouteConfig(configRoute, cluster, route); route.Config = newConfig; - } - - // Check for config changes to the cluster. We don't need a new RouteConfig, but we do need to regenerate - // endpoints that may depend on cluster data. - if (route.ClusterRevision != cluster?.Revision) - { - changed = true; - route.CachedEndpoint = null; // Recreate route.ClusterRevision = cluster?.Revision; } }); @@ -535,7 +527,7 @@ private void UpdateEndpoints(List endpoints) private RouteConfig BuildRouteConfig(ProxyRoute source, ClusterInfo cluster, RouteInfo runtimeRoute) { - var transforms = _transformBuilder.Build(source); + var transforms = _transformBuilder.Build(source, cluster?.Config?.Options); var newRouteConfig = new RouteConfig( runtimeRoute, diff --git a/src/ReverseProxy/Service/Proxy/HttpProxy.cs b/src/ReverseProxy/Service/Proxy/HttpProxy.cs index 4b2580915..cbf36119c 100644 --- a/src/ReverseProxy/Service/Proxy/HttpProxy.cs +++ b/src/ReverseProxy/Service/Proxy/HttpProxy.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Server.Kestrel.Core.Features; +using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -253,7 +254,7 @@ public async Task ProxyAsync( } } - private async Task<(HttpRequestMessage, StreamCopyHttpContent)> CreateRequestMessageAsync(HttpContext context, string destinationPrefix, + private async ValueTask<(HttpRequestMessage, StreamCopyHttpContent)> CreateRequestMessageAsync(HttpContext context, string destinationPrefix, HttpTransformer transformer, RequestProxyOptions requestOptions, bool isStreamingRequest, CancellationToken requestAborted) { // "http://a".Length = 8 @@ -448,7 +449,15 @@ private async Task HandleRequestFailureAsync(HttpContext context, StreamCopyHttp private static Task CopyResponseStatusAndHeadersAsync(HttpResponseMessage source, HttpContext context, HttpTransformer transformer) { context.Response.StatusCode = (int)source.StatusCode; - context.Features.Get().ReasonPhrase = source.ReasonPhrase; + + if (!ProtocolHelper.IsHttp2OrGreater(context.Request.Protocol)) + { + // Don't explicitly set the field if the default reason phrase is used + if (source.ReasonPhrase != ReasonPhrases.GetReasonPhrase((int)source.StatusCode)) + { + context.Features.Get().ReasonPhrase = source.ReasonPhrase; + } + } // Copies headers return transformer.TransformResponseAsync(context, source); @@ -487,8 +496,8 @@ private async Task HandleUpgradedResponse(HttpContext context, HttpResponseMessa using var abortTokenSource = CancellationTokenSource.CreateLinkedTokenSource(longCancellation); - var requestTask = StreamCopier.CopyAsync(isRequest: true, clientStream, destinationStream, _clock, abortTokenSource.Token); - var responseTask = StreamCopier.CopyAsync(isRequest: false, destinationStream, clientStream, _clock, abortTokenSource.Token); + var requestTask = StreamCopier.CopyAsync(isRequest: true, clientStream, destinationStream, _clock, abortTokenSource.Token).AsTask(); + var responseTask = StreamCopier.CopyAsync(isRequest: false, destinationStream, clientStream, _clock, abortTokenSource.Token).AsTask(); // Make sure we report the first failure. var firstTask = await Task.WhenAny(requestTask, responseTask); @@ -526,7 +535,7 @@ void ReportResult(HttpContext context, bool reqeuest, StreamCopyResult result, E } } - private async Task<(StreamCopyResult, Exception)> CopyResponseBodyAsync(HttpContent destinationResponseContent, Stream clientResponseStream, + private async ValueTask<(StreamCopyResult, Exception)> CopyResponseBodyAsync(HttpContent destinationResponseContent, Stream clientResponseStream, CancellationToken cancellation) { // SocketHttpHandler and similar transports always provide an HttpContent object, even if it's empty. diff --git a/src/ReverseProxy/Service/Proxy/HttpTransformer.cs b/src/ReverseProxy/Service/Proxy/HttpTransformer.cs index f2925e326..7de34dc20 100644 --- a/src/ReverseProxy/Service/Proxy/HttpTransformer.cs +++ b/src/ReverseProxy/Service/Proxy/HttpTransformer.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -112,7 +113,8 @@ private static void CopyResponseHeaders(HttpContext httpContext, HttpHeaders sou continue; } - destination.Append(headerName, header.Value.ToArray()); + Debug.Assert(header.Value is string[]); + destination.Append(headerName, header.Value as string[] ?? header.Value.ToArray()); } } } diff --git a/src/ReverseProxy/Service/Proxy/IProxyErrorFeature.cs b/src/ReverseProxy/Service/Proxy/IProxyErrorFeature.cs index 9fa2a5504..f935e21a8 100644 --- a/src/ReverseProxy/Service/Proxy/IProxyErrorFeature.cs +++ b/src/ReverseProxy/Service/Proxy/IProxyErrorFeature.cs @@ -13,11 +13,11 @@ public interface IProxyErrorFeature /// /// The specified ProxyError. /// - public ProxyError Error { get; } + ProxyError Error { get; } /// /// An Exception that occurred when proxying the request to the destination. /// - public Exception Exception { get; } + Exception Exception { get; } } } diff --git a/src/ReverseProxy/Service/Proxy/Infrastructure/ProxyHttpClientFactory.cs b/src/ReverseProxy/Service/Proxy/Infrastructure/ProxyHttpClientFactory.cs index aa8697a37..14993c5b8 100644 --- a/src/ReverseProxy/Service/Proxy/Infrastructure/ProxyHttpClientFactory.cs +++ b/src/ReverseProxy/Service/Proxy/Infrastructure/ProxyHttpClientFactory.cs @@ -6,6 +6,7 @@ using System.Net.Http; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Logging; +using Microsoft.ReverseProxy.Abstractions; using Microsoft.ReverseProxy.Telemetry; namespace Microsoft.ReverseProxy.Service.Proxy.Infrastructure @@ -65,6 +66,10 @@ public HttpMessageInvoker CreateClient(ProxyHttpClientContext context) handler.SslOptions.RemoteCertificateValidationCallback = delegate { return true; }; } #if NET + if (newClientOptions.EnableMultipleHttp2Connections.HasValue) + { + handler.EnableMultipleHttp2Connections = newClientOptions.EnableMultipleHttp2Connections.Value; + } if (newClientOptions.RequestHeaderEncoding != null) { handler.RequestHeaderEncodingSelector = (_, _) => newClientOptions.RequestHeaderEncoding; @@ -73,15 +78,16 @@ public HttpMessageInvoker CreateClient(ProxyHttpClientContext context) Log.ProxyClientCreated(_logger, context.ClusterId); - if (newClientOptions.PropagateActivityContext.GetValueOrDefault(true)) + var activityContextHeaders = newClientOptions.ActivityContextHeaders.GetValueOrDefault(ActivityContextHeaders.BaggageAndCorrelationContext); + if (activityContextHeaders != ActivityContextHeaders.None) { - return new HttpMessageInvoker(new ActivityPropagationHandler(handler), disposeHandler: true); + return new HttpMessageInvoker(new ActivityPropagationHandler(activityContextHeaders, handler), disposeHandler: true); } return new HttpMessageInvoker(handler, disposeHandler: true); } - private bool CanReuseOldClient(ProxyHttpClientContext context) + private static bool CanReuseOldClient(ProxyHttpClientContext context) { return context.OldClient != null && context.NewOptions == context.OldOptions; } diff --git a/src/ReverseProxy/Service/Proxy/StreamCopier.cs b/src/ReverseProxy/Service/Proxy/StreamCopier.cs index 922cf3445..dde6b18a6 100644 --- a/src/ReverseProxy/Service/Proxy/StreamCopier.cs +++ b/src/ReverseProxy/Service/Proxy/StreamCopier.cs @@ -27,7 +27,7 @@ internal static class StreamCopier /// Based on Microsoft.AspNetCore.Http.StreamCopyOperationInternal.CopyToAsync. /// See: . /// - public static async Task<(StreamCopyResult, Exception)> CopyAsync(bool isRequest, Stream input, Stream output, IClock clock, CancellationToken cancellation) + public static async ValueTask<(StreamCopyResult, Exception)> CopyAsync(bool isRequest, Stream input, Stream output, IClock clock, CancellationToken cancellation) { _ = input ?? throw new ArgumentNullException(nameof(input)); _ = output ?? throw new ArgumentNullException(nameof(output)); diff --git a/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs b/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs index 488ba5a5b..4d26e1d37 100644 --- a/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs +++ b/src/ReverseProxy/Service/RuntimeModel/RouteConfig.cs @@ -45,7 +45,7 @@ public RouteConfig( public bool HasConfigChanged(ProxyRoute newConfig, ClusterInfo cluster) { - return Cluster != cluster || !ProxyRoute.Equals(newConfig); + return Cluster != cluster || Route.ClusterRevision != cluster?.Revision || !ProxyRoute.Equals(newConfig); } } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/HttpMethodChangeTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/HttpMethodChangeTransform.cs index a9e63dfc3..ea275b03a 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/HttpMethodChangeTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/HttpMethodChangeTransform.cs @@ -28,14 +28,14 @@ public HttpMethodChangeTransform(string fromMethod, string toMethod) internal HttpMethod ToMethod { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (FromMethod.Equals(context.ProxyRequest.Method)) { context.ProxyRequest.Method = ToMethod; } - return Task.CompletedTask; + return default; } private static HttpMethod GetCanonicalizedValue(string method) diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/NodeFormat.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/NodeFormat.cs index 110c7cd1d..6f8f61e03 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/NodeFormat.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/NodeFormat.cs @@ -11,9 +11,12 @@ public enum NodeFormat None, Random, RandomAndPort, + RandomAndRandomPort, Unknown, UnknownAndPort, + UnknownAndRandomPort, Ip, IpAndPort, + IpAndRandomPort, } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/PathRouteValuesTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/PathRouteValuesTransform.cs index e02ce746f..9f4494f87 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/PathRouteValuesTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/PathRouteValuesTransform.cs @@ -29,7 +29,7 @@ public PathRouteValuesTransform(string pattern, TemplateBinderFactory binderFact internal RouteTemplate Template { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context is null) { @@ -41,7 +41,7 @@ public override Task ApplyAsync(RequestTransformContext context) var binder = _binderFactory.Create(Template, defaults: routeValues); context.Path = binder.BindValues(acceptedValues: routeValues); - return Task.CompletedTask; + return default; } } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/PathStringTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/PathStringTransform.cs index 208addcc0..ad11eec14 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/PathStringTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/PathStringTransform.cs @@ -28,7 +28,7 @@ public PathStringTransform(PathTransformMode mode, PathString value) internal PathTransformMode Mode { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context is null) { @@ -50,7 +50,7 @@ public override Task ApplyAsync(RequestTransformContext context) throw new NotImplementedException(Mode.ToString()); } - return Task.CompletedTask; + return default; } public enum PathTransformMode diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/QueryParameterRemoveTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/QueryParameterRemoveTransform.cs index 1cfebdc70..762dfdd1a 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/QueryParameterRemoveTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/QueryParameterRemoveTransform.cs @@ -18,7 +18,7 @@ public QueryParameterRemoveTransform(string key) internal string Key { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context == null) { @@ -27,7 +27,7 @@ public override Task ApplyAsync(RequestTransformContext context) context.Query.Collection.Remove(Key); - return Task.CompletedTask; + return default; } } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/QueryParameterTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/QueryParameterTransform.cs index 58e1ea518..5e5a1c666 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/QueryParameterTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/QueryParameterTransform.cs @@ -20,7 +20,7 @@ public QueryParameterTransform(QueryStringTransformMode mode, string key) internal string Key { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context == null) { @@ -48,7 +48,7 @@ public override Task ApplyAsync(RequestTransformContext context) } } - return Task.CompletedTask; + return default; } protected abstract string GetValue(RequestTransformContext context); diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestCopyHostTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestCopyHostTransform.cs index eafa803d8..2dc8933c4 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestCopyHostTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestCopyHostTransform.cs @@ -18,7 +18,7 @@ private RequestCopyHostTransform() } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context is null) { @@ -27,7 +27,7 @@ public override Task ApplyAsync(RequestTransformContext context) // Always override the proxy request host with the original request host. context.ProxyRequest.Headers.Host = context.HttpContext.Request.Host.Value; - return Task.CompletedTask; + return default; } } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestFuncTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestFuncTransform.cs index cc5025a68..0fa21878e 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestFuncTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestFuncTransform.cs @@ -11,15 +11,15 @@ namespace Microsoft.ReverseProxy.Service.RuntimeModel.Transforms /// public class RequestFuncTransform : RequestTransform { - private readonly Func _func; + private readonly Func _func; - public RequestFuncTransform(Func func) + public RequestFuncTransform(Func func) { _func = func ?? throw new ArgumentNullException(nameof(func)); } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { return _func(context); } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderClientCertTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderClientCertTransform.cs index 67cab6982..2b039579e 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderClientCertTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderClientCertTransform.cs @@ -20,7 +20,7 @@ public RequestHeaderClientCertTransform(string headerName) internal string HeaderName { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context is null) { @@ -37,7 +37,7 @@ public override Task ApplyAsync(RequestTransformContext context) AddHeader(context, HeaderName, encoded); } - return Task.CompletedTask; + return default; } } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderForwardedTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderForwardedTransform.cs index 0a483602d..965dd70c8 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderForwardedTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderForwardedTransform.cs @@ -43,7 +43,7 @@ public RequestHeaderForwardedTransform(IRandomFactory randomFactory, NodeFormat internal bool Append { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context is null) { @@ -70,7 +70,7 @@ public override Task ApplyAsync(RequestTransformContext context) AddHeader(context, ForwardedHeaderName, value); } - return Task.CompletedTask; + return default; } private void AppendProto(HttpContext context, StringBuilder builder) @@ -131,9 +131,10 @@ private void AppendNode(IPAddress ipAddress, int port, NodeFormat format, String // node-port specified MUST be quoted, since ":" is not an allowed // character in "token"." var addPort = port != 0 && (format == NodeFormat.IpAndPort || format == NodeFormat.UnknownAndPort || format == NodeFormat.RandomAndPort); - var ipv6 = (format == NodeFormat.Ip || format == NodeFormat.IpAndPort) + var addRandomPort = (format == NodeFormat.IpAndRandomPort || format == NodeFormat.UnknownAndRandomPort || format == NodeFormat.RandomAndRandomPort); + var ipv6 = (format == NodeFormat.Ip || format == NodeFormat.IpAndPort || format == NodeFormat.IpAndRandomPort) && ipAddress != null && ipAddress.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6; - var quote = addPort || ipv6; + var quote = addPort || addRandomPort || ipv6; if (quote) { @@ -144,6 +145,7 @@ private void AppendNode(IPAddress ipAddress, int port, NodeFormat format, String { case NodeFormat.Ip: case NodeFormat.IpAndPort: + case NodeFormat.IpAndRandomPort: if (ipAddress != null) { if (ipv6) @@ -161,10 +163,12 @@ private void AppendNode(IPAddress ipAddress, int port, NodeFormat format, String goto case NodeFormat.Unknown; case NodeFormat.Unknown: case NodeFormat.UnknownAndPort: + case NodeFormat.UnknownAndRandomPort: builder.Append("unknown"); break; case NodeFormat.Random: case NodeFormat.RandomAndPort: + case NodeFormat.RandomAndRandomPort: AppendRandom(builder); break; default: @@ -176,6 +180,11 @@ private void AppendNode(IPAddress ipAddress, int port, NodeFormat format, String builder.Append(':'); builder.Append(port); } + else if (addRandomPort) + { + builder.Append(':'); + AppendRandom(builder); + } if (quote) { diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderValueTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderValueTransform.cs index 8cbcc64c2..6818ee56b 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderValueTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderValueTransform.cs @@ -26,7 +26,7 @@ public RequestHeaderValueTransform(string headerName, string value, bool append) internal bool Append { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context is null) { @@ -46,7 +46,7 @@ public override Task ApplyAsync(RequestTransformContext context) AddHeader(context, HeaderName, Value); } - return Task.CompletedTask; + return default; } } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedForTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedForTransform.cs index a3aef22a6..a6989a15e 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedForTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedForTransform.cs @@ -27,7 +27,7 @@ public RequestHeaderXForwardedForTransform(string headerName, bool append) internal bool Append { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context is null) { @@ -56,7 +56,7 @@ public override Task ApplyAsync(RequestTransformContext context) AddHeader(context, HeaderName, remoteIp); } - return Task.CompletedTask; + return default; } } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedHostTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedHostTransform.cs index 1b22eab6c..1c46c4cca 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedHostTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedHostTransform.cs @@ -25,7 +25,7 @@ public RequestHeaderXForwardedHostTransform(string headerName, bool append) internal bool Append { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context is null) { @@ -54,7 +54,7 @@ public override Task ApplyAsync(RequestTransformContext context) AddHeader(context, HeaderName, host.ToUriComponent()); } - return Task.CompletedTask; + return default; } } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedPathBaseTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedPathBaseTransform.cs index 512176bed..98c1507a3 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedPathBaseTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedPathBaseTransform.cs @@ -21,7 +21,7 @@ public RequestHeaderXForwardedPathBaseTransform(string headerName, bool append) internal bool Append { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context is null) { @@ -50,7 +50,7 @@ public override Task ApplyAsync(RequestTransformContext context) AddHeader(context, HeaderName, pathBase.ToUriComponent()); } - return Task.CompletedTask; + return default; } } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedProtoTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedProtoTransform.cs index f9fca7fe8..fe98453de 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedProtoTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestHeaderXForwardedProtoTransform.cs @@ -21,7 +21,7 @@ public RequestHeaderXForwardedProtoTransform(string headerName, bool append) internal bool Append { get; } /// - public override Task ApplyAsync(RequestTransformContext context) + public override ValueTask ApplyAsync(RequestTransformContext context) { if (context is null) { @@ -43,7 +43,7 @@ public override Task ApplyAsync(RequestTransformContext context) AddHeader(context, HeaderName, scheme); } - return Task.CompletedTask; + return default; } } } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestTransform.cs index 1bf91c34b..1208408f4 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/RequestTransform.cs @@ -16,7 +16,7 @@ public abstract class RequestTransform /// /// Transforms any of the available fields before building the outgoing request. /// - public abstract Task ApplyAsync(RequestTransformContext context); + public abstract ValueTask ApplyAsync(RequestTransformContext context); /// /// Removes and returns the current header value by first checking the HttpRequestMessage, diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseFuncTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseFuncTransform.cs index b5fd97a0e..3c415bb93 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseFuncTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseFuncTransform.cs @@ -11,15 +11,15 @@ namespace Microsoft.ReverseProxy.Service.RuntimeModel.Transforms /// public class ResponseFuncTransform : ResponseTransform { - private readonly Func _func; + private readonly Func _func; - public ResponseFuncTransform(Func func) + public ResponseFuncTransform(Func func) { _func = func ?? throw new ArgumentNullException(nameof(func)); } /// - public override Task ApplyAsync(ResponseTransformContext context) + public override ValueTask ApplyAsync(ResponseTransformContext context) { return _func(context); } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseHeaderValueTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseHeaderValueTransform.cs index 167105dc1..c8cd1d4bd 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseHeaderValueTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseHeaderValueTransform.cs @@ -31,7 +31,7 @@ public ResponseHeaderValueTransform(string headerName, string value, bool append // Assumes the response status code has been set on the HttpContext already. /// - public override Task ApplyAsync(ResponseTransformContext context) + public override ValueTask ApplyAsync(ResponseTransformContext context) { if (context is null) { @@ -53,7 +53,7 @@ public override Task ApplyAsync(ResponseTransformContext context) // If the given value is empty, any existing header is removed. } - return Task.CompletedTask; + return default; } private static bool Success(ResponseTransformContext context) diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailerValueTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailerValueTransform.cs index e7460094d..4a97985dd 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailerValueTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailerValueTransform.cs @@ -30,7 +30,7 @@ public ResponseTrailerValueTransform(string headerName, string value, bool appen // Assumes the response status code has been set on the HttpContext already. /// - public override Task ApplyAsync(ResponseTrailersTransformContext context) + public override ValueTask ApplyAsync(ResponseTrailersTransformContext context) { if (context is null) { @@ -52,7 +52,7 @@ public override Task ApplyAsync(ResponseTrailersTransformContext context) // If the given value is empty, any existing header is removed. } - return Task.CompletedTask; + return default; } private static bool Success(ResponseTrailersTransformContext context) diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailersFuncTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailersFuncTransform.cs index 2c0488b0c..8ae9cae1b 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailersFuncTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailersFuncTransform.cs @@ -11,15 +11,15 @@ namespace Microsoft.ReverseProxy.Service.RuntimeModel.Transforms /// public class ResponseTrailersFuncTransform : ResponseTrailersTransform { - private readonly Func _func; + private readonly Func _func; - public ResponseTrailersFuncTransform(Func func) + public ResponseTrailersFuncTransform(Func func) { _func = func ?? throw new ArgumentNullException(nameof(func)); } /// - public override Task ApplyAsync(ResponseTrailersTransformContext context) + public override ValueTask ApplyAsync(ResponseTrailersTransformContext context) { return _func(context); } diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailersTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailersTransform.cs index 752e82f90..2c659aecc 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailersTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTrailersTransform.cs @@ -19,7 +19,7 @@ public abstract class ResponseTrailersTransform /// Transforms the given response trailers. The trailers will have (optionally) already been /// copied to the and any changes should be made there. /// - public abstract Task ApplyAsync(ResponseTrailersTransformContext context); + public abstract ValueTask ApplyAsync(ResponseTrailersTransformContext context); /// /// Removes and returns the current trailer value by first checking the HttpResponse diff --git a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTransform.cs b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTransform.cs index 40766c0b7..2df989f49 100644 --- a/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTransform.cs +++ b/src/ReverseProxy/Service/RuntimeModel/Transforms/ResponseTransform.cs @@ -17,7 +17,7 @@ public abstract class ResponseTransform /// Transforms the given response. The status and headers will have (optionally) already been /// copied to the and any changes should be made there. /// - public abstract Task ApplyAsync(ResponseTransformContext context); + public abstract ValueTask ApplyAsync(ResponseTransformContext context); /// /// Removes and returns the current header value by first checking the HttpResponse diff --git a/src/ReverseProxy/Service/SessionAffinity/AffinitizeTransform.cs b/src/ReverseProxy/Service/SessionAffinity/AffinitizeTransform.cs new file mode 100644 index 000000000..8756cb0e5 --- /dev/null +++ b/src/ReverseProxy/Service/SessionAffinity/AffinitizeTransform.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.ReverseProxy.Middleware; +using Microsoft.ReverseProxy.RuntimeModel; +using Microsoft.ReverseProxy.Service.RuntimeModel.Transforms; + +namespace Microsoft.ReverseProxy.Service.SessionAffinity +{ + /// + /// Affinitizes the request to a chosen . + /// + internal class AffinitizeTransform : ResponseTransform + { + private readonly ISessionAffinityProvider _sessionAffinityProvider; + + public AffinitizeTransform(ISessionAffinityProvider sessionAffinityProvider) + { + _sessionAffinityProvider = sessionAffinityProvider ?? throw new ArgumentNullException(nameof(sessionAffinityProvider)); + } + + public override ValueTask ApplyAsync(ResponseTransformContext context) + { + var proxyFeature = context.HttpContext.GetRequiredProxyFeature(); + var options = proxyFeature.ClusterConfig.Options.SessionAffinity; + // The transform should only be added to routes that have affinity enabled. + Debug.Assert(options?.Enabled ?? true, "Session affinity is not enabled"); + var selectedDestination = proxyFeature.SelectedDestination; + _sessionAffinityProvider.AffinitizeRequest(context.HttpContext, options, selectedDestination); + return default; + } + } +} diff --git a/src/ReverseProxy/Service/SessionAffinity/AffinitizeTransformProvider.cs b/src/ReverseProxy/Service/SessionAffinity/AffinitizeTransformProvider.cs new file mode 100644 index 000000000..b33b24ac5 --- /dev/null +++ b/src/ReverseProxy/Service/SessionAffinity/AffinitizeTransformProvider.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Microsoft.ReverseProxy.Abstractions.ClusterDiscovery.Contract; +using Microsoft.ReverseProxy.Abstractions.Config; +using Microsoft.ReverseProxy.Utilities; + +namespace Microsoft.ReverseProxy.Service.SessionAffinity +{ + internal class AffinitizeTransformProvider : ITransformProvider + { + private readonly IDictionary _sessionAffinityProviders; + + public AffinitizeTransformProvider(IEnumerable sessionAffinityProviders) + { + _sessionAffinityProviders = sessionAffinityProviders?.ToDictionaryByUniqueId(p => p.Mode) + ?? throw new ArgumentNullException(nameof(sessionAffinityProviders)); + } + + public void ValidateRoute(TransformRouteValidationContext context) + { + } + + public void ValidateCluster(TransformClusterValidationContext context) + { + // Other affinity validation logic is covered by ConfigValidator.ValidateSessionAffinity. + if (!(context.Cluster.SessionAffinity?.Enabled ?? false)) + { + return; + } + + var affinityMode = context.Cluster.SessionAffinity.Mode; + if (string.IsNullOrEmpty(affinityMode)) + { + // The default. + affinityMode = SessionAffinityConstants.Modes.Cookie; + } + + if (!_sessionAffinityProviders.ContainsKey(affinityMode)) + { + context.Errors.Add(new ArgumentException($"No matching {nameof(ISessionAffinityProvider)} found for the session affinity mode '{affinityMode}' set on the cluster '{context.Cluster.Id}'.")); + } + } + + public void Apply(TransformBuilderContext context) + { + var options = context.Cluster?.SessionAffinity; + + if ((options?.Enabled).GetValueOrDefault()) + { + var provider = _sessionAffinityProviders.GetRequiredServiceById(options.Mode, SessionAffinityConstants.Modes.Cookie); + context.ResponseTransforms.Add(new AffinitizeTransform(provider)); + } + } + } +} diff --git a/src/ReverseProxy/Service/SessionAffinity/IAffinityFailurePolicy.cs b/src/ReverseProxy/Service/SessionAffinity/IAffinityFailurePolicy.cs index dfcd03fde..bd1e73b76 100644 --- a/src/ReverseProxy/Service/SessionAffinity/IAffinityFailurePolicy.cs +++ b/src/ReverseProxy/Service/SessionAffinity/IAffinityFailurePolicy.cs @@ -16,7 +16,7 @@ public interface IAffinityFailurePolicy /// /// A unique identifier for this failure policy. This will be referenced from config. /// - public string Name { get; } + string Name { get; } /// /// Handles affinity failures. This method assumes the full control on @@ -29,6 +29,6 @@ public interface IAffinityFailurePolicy /// 'true' if the failure is considered recoverable and the request processing can proceed. /// Otherwise, 'false' indicating that an error response has been generated and the request's processing must be terminated. /// - public Task Handle(HttpContext context, SessionAffinityOptions options, AffinityStatus affinityStatus); + Task Handle(HttpContext context, SessionAffinityOptions options, AffinityStatus affinityStatus); } } diff --git a/src/ReverseProxy/Service/SessionAffinity/ISessionAffinityProvider.cs b/src/ReverseProxy/Service/SessionAffinity/ISessionAffinityProvider.cs index 26b89fd20..45cfa72d8 100644 --- a/src/ReverseProxy/Service/SessionAffinity/ISessionAffinityProvider.cs +++ b/src/ReverseProxy/Service/SessionAffinity/ISessionAffinityProvider.cs @@ -16,7 +16,7 @@ public interface ISessionAffinityProvider /// /// A unique identifier for this session affinity implementation. This will be referenced from config. /// - public string Mode { get; } + string Mode { get; } /// /// Finds to which the current request is affinitized by the affinity key. @@ -26,7 +26,7 @@ public interface ISessionAffinityProvider /// Target cluster ID. /// Affinity options. /// carrying the found affinitized destinations if any and the . - public AffinityResult FindAffinitizedDestinations(HttpContext context, IReadOnlyList destinations, string clusterId, SessionAffinityOptions options); + AffinityResult FindAffinitizedDestinations(HttpContext context, IReadOnlyList destinations, string clusterId, SessionAffinityOptions options); /// /// Affinitize the current request to the given by setting the affinity key extracted from . @@ -34,6 +34,6 @@ public interface ISessionAffinityProvider /// Current request's context. /// Affinity options. /// to which request is to be affinitized. - public void AffinitizeRequest(HttpContext context, SessionAffinityOptions options, DestinationInfo destination); + void AffinitizeRequest(HttpContext context, SessionAffinityOptions options, DestinationInfo destination); } } diff --git a/src/ReverseProxy/Telemetry/ActivityPropagationHandler.cs b/src/ReverseProxy/Telemetry/ActivityPropagationHandler.cs index f63eb4dd3..57c6e59a0 100644 --- a/src/ReverseProxy/Telemetry/ActivityPropagationHandler.cs +++ b/src/ReverseProxy/Telemetry/ActivityPropagationHandler.cs @@ -8,6 +8,7 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; +using Microsoft.ReverseProxy.Abstractions; namespace Microsoft.ReverseProxy.Telemetry { @@ -18,12 +19,16 @@ public sealed class ActivityPropagationHandler : DelegatingHandler { private const string RequestIdHeaderName = "Request-Id"; private const string CorrelationContextHeaderName = "Correlation-Context"; + private const string BaggageHeaderName = "baggage"; private const string TraceParentHeaderName = "traceparent"; private const string TraceStateHeaderName = "tracestate"; - public ActivityPropagationHandler(HttpMessageHandler innerHandler) : base(innerHandler) + private readonly ActivityContextHeaders _activityContextHeaders; + + public ActivityPropagationHandler(ActivityContextHeaders activityContextHeaders, HttpMessageHandler innerHandler) : base(innerHandler) { + _activityContextHeaders = activityContextHeaders; } protected override Task SendAsync(HttpRequestMessage request, @@ -79,7 +84,14 @@ private void InjectHeaders(Activity currentActivity, HttpRequestMessage request) baggage.Add(new NameValueHeaderValue(Uri.EscapeDataString(item.Key), Uri.EscapeDataString(item.Value)).ToString()); } while (e.MoveNext()); - request.Headers.TryAddWithoutValidation(CorrelationContextHeaderName, baggage); + if (_activityContextHeaders.HasFlag(ActivityContextHeaders.Baggage)) + { + request.Headers.TryAddWithoutValidation(BaggageHeaderName, baggage); + } + if (_activityContextHeaders.HasFlag(ActivityContextHeaders.CorrelationContext)) + { + request.Headers.TryAddWithoutValidation(CorrelationContextHeaderName, baggage); + } } } } diff --git a/src/ReverseProxy/Utilities/RequestUtilities.cs b/src/ReverseProxy/Utilities/RequestUtilities.cs index 835d39be1..fb6cd28e6 100644 --- a/src/ReverseProxy/Utilities/RequestUtilities.cs +++ b/src/ReverseProxy/Utilities/RequestUtilities.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Net.Http; -using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -43,23 +42,16 @@ internal static bool ShouldSkipResponseHeader(string headerName, bool isHttp2OrG /// The query to append internal static Uri MakeDestinationAddress(string destinationPrefix, PathString path, QueryString query) { - var builder = new StringBuilder(destinationPrefix); - if (path.HasValue) - { - // When PathString has a value it always starts with a '/'. Avoid double slashes when concatenating. - if (builder.Length > 0 && builder[^1] == '/') - { - builder.Length--; - } + ReadOnlySpan prefixSpan = destinationPrefix; - builder.Append(path.ToUriComponent()); - } - if (query.HasValue) + if (path.HasValue && destinationPrefix.EndsWith('/')) { - builder.Append(query.ToUriComponent()); + // When PathString has a value it always starts with a '/'. Avoid double slashes when concatenating. + prefixSpan = prefixSpan[0..^1]; } - var targetAddress = builder.ToString(); + var targetAddress = string.Concat(prefixSpan, path.ToUriComponent(), query.ToUriComponent()); + return new Uri(targetAddress, UriKind.Absolute); } diff --git a/src/ReverseProxy/Utilities/ServiceLookupHelper.cs b/src/ReverseProxy/Utilities/ServiceLookupHelper.cs index 0c1ff35e9..bdf406383 100644 --- a/src/ReverseProxy/Utilities/ServiceLookupHelper.cs +++ b/src/ReverseProxy/Utilities/ServiceLookupHelper.cs @@ -43,7 +43,7 @@ public static T GetRequiredServiceById(this IDictionary services, if (!services.TryGetValue(lookup, out var result)) { - throw new ArgumentException($"No {typeof(T)} was found for the id {lookup}.", nameof(id)); + throw new ArgumentException($"No {typeof(T)} was found for the id '{lookup}'.", nameof(id)); } return result; } diff --git a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ClusterTests.cs b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ClusterTests.cs index d2a5cd004..fbb53c036 100644 --- a/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ClusterTests.cs +++ b/test/ReverseProxy.Tests/Abstractions/ClusterDiscovery/Contract/ClusterTests.cs @@ -71,7 +71,7 @@ public void Equals_Same_Value_Returns_True() SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, - PropagateActivityContext = true, + ActivityContextHeaders = ActivityContextHeaders.CorrelationContext, #if NET RequestHeaderEncoding = Encoding.UTF8 #endif @@ -141,7 +141,7 @@ public void Equals_Same_Value_Returns_True() SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, - PropagateActivityContext = true, + ActivityContextHeaders = ActivityContextHeaders.CorrelationContext, #if NET RequestHeaderEncoding = Encoding.UTF8 #endif @@ -221,7 +221,7 @@ public void Equals_Different_Value_Returns_False() SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, - PropagateActivityContext = true, + ActivityContextHeaders = ActivityContextHeaders.CorrelationContext, }, HttpRequest = new RequestProxyOptions { @@ -255,7 +255,7 @@ public void Equals_Different_Value_Returns_False() SslProtocols = SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, - PropagateActivityContext = true, + ActivityContextHeaders = ActivityContextHeaders.CorrelationContext, } })); Assert.False(options1.Equals(options1 with { HttpRequest = new RequestProxyOptions() { } })); diff --git a/test/ReverseProxy.Tests/Abstractions/Config/TransformBuilderContextFuncExtensionsTests.cs b/test/ReverseProxy.Tests/Abstractions/Config/TransformBuilderContextFuncExtensionsTests.cs index a8448d957..1b1c1523d 100644 --- a/test/ReverseProxy.Tests/Abstractions/Config/TransformBuilderContextFuncExtensionsTests.cs +++ b/test/ReverseProxy.Tests/Abstractions/Config/TransformBuilderContextFuncExtensionsTests.cs @@ -15,7 +15,7 @@ public void AddRequestTransform() var builderContext = CreateBuilderContext(); builderContext.AddRequestTransform(context => { - return Task.CompletedTask; + return default; }); var requestTransform = Assert.Single(builderContext.RequestTransforms); @@ -28,7 +28,7 @@ public void AddResponseTransform() var builderContext = CreateBuilderContext(); builderContext.AddResponseTransform(context => { - return Task.CompletedTask; + return default; }); var responseTransform = Assert.Single(builderContext.ResponseTransforms); @@ -41,7 +41,7 @@ public void AddResponseTrailersTransform() var builderContext = CreateBuilderContext(); builderContext.AddResponseTrailersTransform(context => { - return Task.CompletedTask; + return default; }); var responseTrailersTransform = Assert.Single(builderContext.ResponseTrailersTransforms); diff --git a/test/ReverseProxy.Tests/Abstractions/Config/TransformExtentionsTestsBase.cs b/test/ReverseProxy.Tests/Abstractions/Config/TransformExtentionsTestsBase.cs index 6bb00f864..e1c75fe29 100644 --- a/test/ReverseProxy.Tests/Abstractions/Config/TransformExtentionsTestsBase.cs +++ b/test/ReverseProxy.Tests/Abstractions/Config/TransformExtentionsTestsBase.cs @@ -18,7 +18,7 @@ protected static TransformBuilderContext ValidateAndBuild(ProxyRoute proxyRoute, { var transformValues = Assert.Single(proxyRoute.Transforms); - var validationContext = new TransformValidationContext { Route = proxyRoute }; + var validationContext = new TransformRouteValidationContext { Route = proxyRoute }; Assert.True(factory.Validate(validationContext, transformValues)); Assert.Empty(validationContext.Errors); diff --git a/test/ReverseProxy.Tests/Configuration/ConfigurationConfigProviderTests.cs b/test/ReverseProxy.Tests/Configuration/ConfigurationConfigProviderTests.cs index c55a6cee8..737f709a5 100644 --- a/test/ReverseProxy.Tests/Configuration/ConfigurationConfigProviderTests.cs +++ b/test/ReverseProxy.Tests/Configuration/ConfigurationConfigProviderTests.cs @@ -87,9 +87,9 @@ public class ConfigurationConfigProviderTests SslProtocols = SslProtocols.Tls11 | SslProtocols.Tls12, MaxConnectionsPerServer = 10, DangerousAcceptAnyServerCertificate = true, - PropagateActivityContext = true, + ActivityContextHeaders = ActivityContextHeaders.Baggage, #if NET - RequestHeaderEncoding = Encoding.UTF8 + EnableMultipleHttp2Connections = true, #endif }, HttpRequest = new RequestProxyOptions() @@ -216,8 +216,9 @@ public class ConfigurationConfigProviderTests ""AllowInvalid"": null }, ""MaxConnectionsPerServer"": 10, - ""PropagateActivityContext"": true, - ""RequestHeaderEncoding"": ""utf-8"", + ""EnableMultipleHttp2Connections"": true, + ""ActivityContextHeaders"": ""Baggage"", + ""RequestHeaderEncoding"": ""utf-8"" }, ""HttpRequest"": { ""Timeout"": ""00:01:00"", @@ -599,11 +600,12 @@ private void VerifyValidAbstractConfig(IProxyConfig validConfig, X509Certificate Assert.Equal(cluster1.SessionAffinity.Settings, abstractCluster1.SessionAffinity.Settings); Assert.Same(certificate, abstractCluster1.HttpClient.ClientCertificate); Assert.Equal(cluster1.HttpClient.MaxConnectionsPerServer, abstractCluster1.HttpClient.MaxConnectionsPerServer); - Assert.Equal(cluster1.HttpClient.PropagateActivityContext, abstractCluster1.HttpClient.PropagateActivityContext); - Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, abstractCluster1.HttpClient.SslProtocols); #if NET + Assert.Equal(cluster1.HttpClient.EnableMultipleHttp2Connections, abstractCluster1.HttpClient.EnableMultipleHttp2Connections); Assert.Equal(Encoding.UTF8, abstractCluster1.HttpClient.RequestHeaderEncoding); #endif + Assert.Equal(cluster1.HttpClient.ActivityContextHeaders, abstractCluster1.HttpClient.ActivityContextHeaders); + Assert.Equal(SslProtocols.Tls11 | SslProtocols.Tls12, abstractCluster1.HttpClient.SslProtocols); Assert.Equal(cluster1.HttpRequest.Timeout, abstractCluster1.HttpRequest.Timeout); Assert.Equal(HttpVersion.Version10, abstractCluster1.HttpRequest.Version); #if NET diff --git a/test/ReverseProxy.Tests/Middleware/AffinitizeRequestMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/AffinitizeRequestMiddlewareTests.cs deleted file mode 100644 index b69fadb32..000000000 --- a/test/ReverseProxy.Tests/Middleware/AffinitizeRequestMiddlewareTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Microsoft.ReverseProxy.RuntimeModel; -using Microsoft.ReverseProxy.Service.SessionAffinity; -using Moq; -using Xunit; - -namespace Microsoft.ReverseProxy.Middleware -{ - public class AffinitizeRequestMiddlewareTests : AffinityMiddlewareTestBase - { - [Fact] - public async Task Invoke_SingleDestinationChosen_InvokeAffinitizeRequest() - { - var cluster = GetCluster(); - var invokedMode = string.Empty; - const string expectedMode = "Mode-B"; - var providers = RegisterAffinityProviders( - false, - cluster.DestinationManager.Items[1], - cluster.ClusterId, - ("Mode-A", (AffinityStatus?)null, (DestinationInfo[])null, (Action)(p => throw new InvalidOperationException($"Provider {p.Mode} call is not expected."))), - (expectedMode, (AffinityStatus?)null, (DestinationInfo[])null, (Action)(p => invokedMode = p.Mode))); - var nextInvoked = false; - var middleware = new AffinitizeRequestMiddleware(c => { - nextInvoked = true; - return Task.CompletedTask; - }, - providers.Select(p => p.Object), - new Mock>().Object); - var context = new DefaultHttpContext(); - context.Features.Set(cluster); - var destinationFeature = GetDestinationsFeature(cluster.DestinationManager.Items[1], cluster.Config); - context.Features.Set(destinationFeature); - - await middleware.Invoke(context); - - Assert.Equal(expectedMode, invokedMode); - Assert.True(nextInvoked); - providers[0].VerifyGet(p => p.Mode, Times.Once); - providers[0].VerifyNoOtherCalls(); - providers[1].VerifyAll(); - Assert.Same(destinationFeature.AvailableDestinations, cluster.DestinationManager.Items[1]); - } - - [Fact] - public async Task Invoke_MultipleCandidateDestinations_ChooseOneAndInvokeAffinitizeRequest() - { - var cluster = GetCluster(); - var endpoint = GetEndpoint(cluster); - var invokedMode = string.Empty; - const string expectedMode = "Mode-B"; - var providers = new[] { - GetProviderForRandomDestination("Mode-A", cluster.DestinationManager.Items, p => throw new InvalidOperationException($"Provider {p.Mode} call is not expected.")), - GetProviderForRandomDestination(expectedMode, cluster.DestinationManager.Items, p => invokedMode = p.Mode) - }; - var nextInvoked = false; - var logger = AffinityTestHelper.GetLogger(); - var middleware = new AffinitizeRequestMiddleware(c => { - nextInvoked = true; - return Task.CompletedTask; - }, - providers.Select(p => p.Object), - logger.Object); - var context = new DefaultHttpContext(); - context.SetEndpoint(endpoint); - var destinationFeature = GetDestinationsFeature(cluster.DestinationManager.Items, cluster.Config); - context.Features.Set(destinationFeature); - - await middleware.Invoke(context); - - Assert.Equal(expectedMode, invokedMode); - Assert.True(nextInvoked); - providers[0].VerifyGet(p => p.Mode, Times.Once); - providers[0].VerifyNoOtherCalls(); - providers[1].VerifyAll(); - logger.Verify( - l => l.Log(LogLevel.Warning, EventIds.MultipleDestinationsOnClusterToEstablishRequestAffinity, It.IsAny(), null, (Func)It.IsAny()), - Times.Once); - Assert.Equal(1, destinationFeature.AvailableDestinations.Count); - var chosen = destinationFeature.AvailableDestinations[0]; - var sameDestinationCount = cluster.DestinationManager.Items.Count(d => chosen == d); - Assert.Equal(1, sameDestinationCount); - } - - [Fact] - public async Task Invoke_NoDestinationChosen_LogWarningAndCallNext() - { - var cluster = GetCluster(); - var endpoint = GetEndpoint(cluster); - var nextInvoked = false; - var logger = AffinityTestHelper.GetLogger(); - var middleware = new AffinitizeRequestMiddleware(c => { - nextInvoked = true; - return Task.CompletedTask; - }, - new ISessionAffinityProvider[0], - logger.Object); - var context = new DefaultHttpContext(); - context.SetEndpoint(endpoint); - var destinationFeature = GetDestinationsFeature(new DestinationInfo[0], cluster.Config); - context.Features.Set(destinationFeature); - - await middleware.Invoke(context); - - Assert.True(nextInvoked); - logger.Verify( - l => l.Log(LogLevel.Warning, EventIds.NoDestinationOnClusterToEstablishRequestAffinity, It.IsAny(), null, (Func)It.IsAny()), - Times.Once); - Assert.Equal(0, destinationFeature.AvailableDestinations.Count); - } - - private Mock GetProviderForRandomDestination(string mode, IReadOnlyList destinations, Action callback) - { - var provider = new Mock(MockBehavior.Strict); - provider.SetupGet(p => p.Mode).Returns(mode); - provider.Setup(p => p.AffinitizeRequest(It.IsAny(), ClusterConfig.Options.SessionAffinity, It.Is(d => destinations.Contains(d)))) - .Callback(() => callback(provider.Object)); - return provider; - } - } -} diff --git a/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs b/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs index 58ac3a548..a46e97e77 100644 --- a/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs +++ b/test/ReverseProxy.Tests/Middleware/LoadBalancerMiddlewareTests.cs @@ -50,7 +50,7 @@ public async Task PickDestination_UnsupportedPolicy_Throws() var sut = CreateMiddleware(); var ex = await Assert.ThrowsAsync(async () => await sut.Invoke(context)); - Assert.Equal($"No {typeof(ILoadBalancingPolicy)} was found for the id {PolicyName}. (Parameter 'id')", ex.Message); + Assert.Equal($"No {typeof(ILoadBalancingPolicy)} was found for the id '{PolicyName}'. (Parameter 'id')", ex.Message); } [Fact] diff --git a/test/ReverseProxy.Tests/Service/Config/ConfigValidatorTests.cs b/test/ReverseProxy.Tests/Service/Config/ConfigValidatorTests.cs index 022f3e87f..9a9038b32 100644 --- a/test/ReverseProxy.Tests/Service/Config/ConfigValidatorTests.cs +++ b/test/ReverseProxy.Tests/Service/Config/ConfigValidatorTests.cs @@ -571,28 +571,6 @@ public async Task EnableSessionAffinity_Works() Assert.Empty(errors); } - [Fact] - public async Task EnableSession_InvalidMode_Fails() - { - var services = CreateServices(); - var validator = services.GetRequiredService(); - - var cluster = new Cluster - { - Id = "cluster1", - SessionAffinity = new SessionAffinityOptions() - { - Enabled = true, - Mode = "Invalid" - } - }; - - var errors = await validator.ValidateClusterAsync(cluster); - - var ex = Assert.Single(errors); - Assert.Equal("No matching ISessionAffinityProvider found for the session affinity mode 'Invalid' set on the cluster 'cluster1'.", ex.Message); - } - [Fact] public async Task EnableSession_InvalidPolicy_Fails() { diff --git a/test/ReverseProxy.Tests/Service/Config/TransformBuilderTests.cs b/test/ReverseProxy.Tests/Service/Config/TransformBuilderTests.cs index 154578d5b..3067740a4 100644 --- a/test/ReverseProxy.Tests/Service/Config/TransformBuilderTests.cs +++ b/test/ReverseProxy.Tests/Service/Config/TransformBuilderTests.cs @@ -40,10 +40,10 @@ private void NullOrEmptyTransforms_AddsDefaults(IReadOnlyList(() => transformBuilder.BuildInternal(route)); + var nie = Assert.Throws(() => transformBuilder.BuildInternal(route, new Cluster())); Assert.Equal("Unknown transform: ", nie.Message); } @@ -102,12 +102,12 @@ public void UnknownTransforms_Error() }; var route = new ProxyRoute() { Transforms = transforms }; - var errors = transformBuilder.Validate(route); + var errors = transformBuilder.ValidateRoute(route); //All errors reported Assert.Equal(2, errors.Count); Assert.Equal("Unknown transform: string1;string2", errors.First().Message); Assert.Equal("Unknown transform: string3;string4", errors.Skip(1).First().Message); - var ex = Assert.Throws(() => transformBuilder.BuildInternal(route)); + var ex = Assert.Throws(() => transformBuilder.BuildInternal(route, new Cluster())); // First error reported Assert.Equal("Unknown transform: string1;string2", ex.Message); } @@ -125,13 +125,13 @@ public void CallsTransformFactories() { transform["2"] = "B"; }); - var errors = builder.Validate(route); + var errors = builder.ValidateRoute(route); Assert.Empty(errors); Assert.Equal(1, factory1.ValidationCalls); Assert.Equal(1, factory2.ValidationCalls); Assert.Equal(0, factory3.ValidationCalls); - var transforms = builder.BuildInternal(route); + var transforms = builder.BuildInternal(route, new Cluster()); Assert.Equal(1, factory1.BuildCalls); Assert.Equal(1, factory2.BuildCalls); Assert.Equal(0, factory3.BuildCalls); @@ -149,13 +149,20 @@ public void CallsTransformProviders() Array.Empty(), new[] { provider1, provider2, provider3 }); var route = new ProxyRoute(); - var errors = builder.Validate(route); + var errors = builder.ValidateRoute(route); Assert.Empty(errors); - Assert.Equal(1, provider1.ValidationCalls); - Assert.Equal(1, provider2.ValidationCalls); - Assert.Equal(1, provider3.ValidationCalls); + Assert.Equal(1, provider1.ValidateRouteCalls); + Assert.Equal(1, provider2.ValidateRouteCalls); + Assert.Equal(1, provider3.ValidateRouteCalls); - var transforms = builder.BuildInternal(route); + var cluster = new Cluster(); + errors = builder.ValidateCluster(cluster); + Assert.Empty(errors); + Assert.Equal(1, provider1.ValidateClusterCalls); + Assert.Equal(1, provider2.ValidateClusterCalls); + Assert.Equal(1, provider3.ValidateClusterCalls); + + var transforms = builder.BuildInternal(route, cluster); Assert.Equal(1, provider1.ApplyCalls); Assert.Equal(1, provider2.ApplyCalls); Assert.Equal(1, provider3.ApplyCalls); @@ -180,10 +187,10 @@ public void DefaultsCanBeDisabled() }; var route = new ProxyRoute() { Transforms = transforms }; - var errors = transformBuilder.Validate(route); + var errors = transformBuilder.ValidateRoute(route); Assert.Empty(errors); - var results = transformBuilder.BuildInternal(route); + var results = transformBuilder.BuildInternal(route, new Cluster()); Assert.NotNull(results); Assert.Null(results.ShouldCopyRequestHeaders); Assert.Empty(results.RequestTransforms); @@ -216,10 +223,10 @@ public void UseOriginalHost(bool useOriginalHost, bool copyHeaders) }; var route = new ProxyRoute() { Transforms = transforms }; - var errors = transformBuilder.Validate(route); + var errors = transformBuilder.ValidateRoute(route); Assert.Empty(errors); - var results = transformBuilder.BuildInternal(route); + var results = transformBuilder.BuildInternal(route, new Cluster()); Assert.NotNull(results); Assert.Equal(copyHeaders, results.ShouldCopyRequestHeaders); Assert.Empty(results.ResponseTransforms); @@ -261,10 +268,10 @@ public void DefaultsCanBeOverridenByForwarded() }; var route = new ProxyRoute() { Transforms = transforms }; - var errors = transformBuilder.Validate(route); + var errors = transformBuilder.ValidateRoute(route); Assert.Empty(errors); - var results = transformBuilder.BuildInternal(route); + var results = transformBuilder.BuildInternal(route, new Cluster()); var transform = Assert.Single(results.RequestTransforms); var forwardedTransform = Assert.IsType(transform); Assert.True(forwardedTransform.ProtoEnabled); @@ -273,6 +280,7 @@ public void DefaultsCanBeOverridenByForwarded() private static TransformBuilder CreateTransformBuilder() { var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); serviceCollection.AddReverseProxy(); using var services = serviceCollection.BuildServiceProvider(); return (TransformBuilder)services.GetRequiredService(); @@ -290,7 +298,7 @@ public TestTransformFactory(string v) _v = v; } - public bool Validate(TransformValidationContext context, IReadOnlyDictionary transformValues) + public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { Assert.NotNull(context.Services); Assert.NotNull(context.Route); @@ -306,7 +314,7 @@ public bool Build(TransformBuilderContext context, IReadOnlyDictionary Task.CompletedTask); + context.AddResponseTrailersTransform(context => default); return true; } @@ -316,21 +324,31 @@ public bool Build(TransformBuilderContext context, IReadOnlyDictionary>()); diff --git a/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs b/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs index 31d4c4c60..e426f070d 100644 --- a/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs +++ b/test/ReverseProxy.Tests/Service/Management/ProxyConfigManagerTests.cs @@ -7,7 +7,6 @@ using System.Security.Authentication; using System.Text; using System.Threading; -using System.Threading.Channels; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Routing; diff --git a/test/ReverseProxy.Tests/Service/Proxy/HttpProxyTests.cs b/test/ReverseProxy.Tests/Service/Proxy/HttpProxyTests.cs index 7510e62cb..2f3f65fc3 100644 --- a/test/ReverseProxy.Tests/Service/Proxy/HttpProxyTests.cs +++ b/test/ReverseProxy.Tests/Service/Proxy/HttpProxyTests.cs @@ -203,7 +203,7 @@ public async Task ProxyAsync_NormalRequestWithTransforms_Works() Assert.Equal(234, httpContext.Response.StatusCode); var reasonPhrase = httpContext.Features.Get().ReasonPhrase; - Assert.Equal("Test Reason Phrase", reasonPhrase); + Assert.Null(reasonPhrase); // We don't set the ReasonPhrase for HTTP/2+ Assert.Equal(new[] { "response", "value" }, httpContext.Response.Headers["x-ms-response-test"].ToArray()); Assert.Contains("responseLanguage", httpContext.Response.Headers["Content-Language"].ToArray()); Assert.Contains("value", httpContext.Response.Headers["transformHeader"].ToArray()); diff --git a/test/ReverseProxy.Tests/Service/Proxy/Infrastructure/ProxyHttpClientFactoryTests.cs b/test/ReverseProxy.Tests/Service/Proxy/Infrastructure/ProxyHttpClientFactoryTests.cs index 6f28c4562..822a4a4c5 100644 --- a/test/ReverseProxy.Tests/Service/Proxy/Infrastructure/ProxyHttpClientFactoryTests.cs +++ b/test/ReverseProxy.Tests/Service/Proxy/Infrastructure/ProxyHttpClientFactoryTests.cs @@ -53,7 +53,7 @@ public void CreateClient_ApplySslProtocols_Success() var factory = new ProxyHttpClientFactory(Mock>().Object); var options = new ProxyHttpClientOptions { - SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13, + SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13, }; var client = factory.CreateClient(new ProxyHttpClientContext { NewOptions = options }); @@ -113,7 +113,7 @@ public void CreateClient_ApplyMaxConnectionsPerServer_Success() public void CreateClient_ApplyPropagateActivityContext_Success() { var factory = new ProxyHttpClientFactory(Mock>().Object); - var options = new ProxyHttpClientOptions { PropagateActivityContext = false }; + var options = new ProxyHttpClientOptions { ActivityContextHeaders = ActivityContextHeaders.None }; var client = factory.CreateClient(new ProxyHttpClientContext { NewOptions = options }); var handler = GetHandler(client, expectActivityPropagationHandler: false); @@ -153,7 +153,7 @@ public void CreateClient_OldClientExistsNoConfigChange_ReturnsOldInstance() DangerousAcceptAnyServerCertificate = true, ClientCertificate = clientCertificate, MaxConnectionsPerServer = 10, - PropagateActivityContext = true, + ActivityContextHeaders = ActivityContextHeaders.CorrelationContext, #if NET RequestHeaderEncoding = Encoding.Latin1, #endif @@ -169,6 +169,22 @@ public void CreateClient_OldClientExistsNoConfigChange_ReturnsOldInstance() Assert.Same(oldClient, actualClient); } +#if NET + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CreateClient_ApplyEnableMultipleHttp2Connections_Success(bool enableMultipleHttp2Connections) + { + var factory = new ProxyHttpClientFactory(Mock>().Object); + var options = new ProxyHttpClientOptions { EnableMultipleHttp2Connections = enableMultipleHttp2Connections }; + var client = factory.CreateClient(new ProxyHttpClientContext { NewOptions = options }); + + var handler = GetHandler(client); + + Assert.Equal(enableMultipleHttp2Connections, handler.EnableMultipleHttp2Connections); + } +#endif + [Theory] [MemberData(nameof(GetChangedHttpClientOptions))] public void CreateClient_OldClientExistsHttpClientOptionsChanged_ReturnsNewInstance(ProxyHttpClientOptions oldOptions, ProxyHttpClientOptions newOptions) @@ -195,7 +211,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = clientCertificate, MaxConnectionsPerServer = null, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, new ProxyHttpClientOptions { @@ -203,7 +219,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = clientCertificate, MaxConnectionsPerServer = null, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, }, new object[] { @@ -213,7 +229,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = clientCertificate, MaxConnectionsPerServer = null, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, new ProxyHttpClientOptions { @@ -221,7 +237,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = false, ClientCertificate = clientCertificate, MaxConnectionsPerServer = null, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, }, new object[] { @@ -231,7 +247,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = clientCertificate, MaxConnectionsPerServer = null, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, new ProxyHttpClientOptions { @@ -239,7 +255,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = false, ClientCertificate = null, MaxConnectionsPerServer = null, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, }, new object[] { @@ -249,7 +265,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = null, MaxConnectionsPerServer = null, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, new ProxyHttpClientOptions { @@ -257,7 +273,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = false, ClientCertificate = clientCertificate, MaxConnectionsPerServer = null, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, }, new object[] { @@ -267,7 +283,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = null, MaxConnectionsPerServer = null, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, new ProxyHttpClientOptions { @@ -275,7 +291,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = clientCertificate, MaxConnectionsPerServer = 10, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, }, new object[] { @@ -285,7 +301,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = null, MaxConnectionsPerServer = 10, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, new ProxyHttpClientOptions { @@ -293,7 +309,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = clientCertificate, MaxConnectionsPerServer = null, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, }, new object[] { @@ -303,7 +319,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = null, MaxConnectionsPerServer = 10, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, }, new ProxyHttpClientOptions { @@ -311,7 +327,25 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = clientCertificate, MaxConnectionsPerServer = 20, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, + }, + }, + new object[] { + new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls11, + DangerousAcceptAnyServerCertificate = true, + ClientCertificate = null, + MaxConnectionsPerServer = 10, + ActivityContextHeaders = ActivityContextHeaders.CorrelationContext, + }, + new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls11, + DangerousAcceptAnyServerCertificate = true, + ClientCertificate = null, + MaxConnectionsPerServer = 10, + ActivityContextHeaders = ActivityContextHeaders.None, }, }, new object[] { @@ -321,7 +355,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = null, MaxConnectionsPerServer = 10, - PropagateActivityContext = true, + ActivityContextHeaders = ActivityContextHeaders.Baggage, }, new ProxyHttpClientOptions { @@ -329,7 +363,25 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = null, MaxConnectionsPerServer = 10, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.CorrelationContext, + }, + }, + new object[] { + new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls11, + DangerousAcceptAnyServerCertificate = true, + ClientCertificate = null, + MaxConnectionsPerServer = 10, + ActivityContextHeaders = ActivityContextHeaders.CorrelationContext, + }, + new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls11, + DangerousAcceptAnyServerCertificate = true, + ClientCertificate = null, + MaxConnectionsPerServer = 10, + ActivityContextHeaders = ActivityContextHeaders.BaggageAndCorrelationContext, }, }, #if NET @@ -340,7 +392,27 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = null, MaxConnectionsPerServer = 10, - PropagateActivityContext = true, + ActivityContextHeaders = ActivityContextHeaders.Baggage, + EnableMultipleHttp2Connections = true + }, + new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls11, + DangerousAcceptAnyServerCertificate = true, + ClientCertificate = null, + MaxConnectionsPerServer = 10, + ActivityContextHeaders = ActivityContextHeaders.Baggage, + EnableMultipleHttp2Connections = false + }, + }, + new object[] { + new ProxyHttpClientOptions + { + SslProtocols = SslProtocols.Tls11, + DangerousAcceptAnyServerCertificate = true, + ClientCertificate = null, + MaxConnectionsPerServer = 10, + ActivityContextHeaders = ActivityContextHeaders.None, }, new ProxyHttpClientOptions { @@ -348,7 +420,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = null, MaxConnectionsPerServer = 10, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, RequestHeaderEncoding = Encoding.UTF8, }, }, @@ -359,7 +431,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = null, MaxConnectionsPerServer = 10, - PropagateActivityContext = true, + ActivityContextHeaders = ActivityContextHeaders.None, RequestHeaderEncoding = Encoding.UTF8, }, new ProxyHttpClientOptions @@ -368,7 +440,7 @@ public static IEnumerable GetChangedHttpClientOptions() DangerousAcceptAnyServerCertificate = true, ClientCertificate = null, MaxConnectionsPerServer = 10, - PropagateActivityContext = false, + ActivityContextHeaders = ActivityContextHeaders.None, RequestHeaderEncoding = Encoding.Latin1, }, } @@ -395,7 +467,7 @@ private void VerifyDefaultValues(SocketsHttpHandler actualHandler, params string { var skippedSet = new HashSet(skippedExtractors); var defaultHandler = new SocketsHttpHandler(); - foreach(var extractor in GetAllExtractors().Where(e => !skippedSet.Contains(e.name)).Select(e => e.extractor)) + foreach (var extractor in GetAllExtractors().Where(e => !skippedSet.Contains(e.name)).Select(e => e.extractor)) { Assert.Equal(extractor(defaultHandler), extractor(actualHandler)); } diff --git a/test/ReverseProxy.Tests/Service/RuntimeModel/Transforms/RequestHeaderForwardedTransformTests.cs b/test/ReverseProxy.Tests/Service/RuntimeModel/Transforms/RequestHeaderForwardedTransformTests.cs index 71b78b5e7..074e0ab13 100644 --- a/test/ReverseProxy.Tests/Service/RuntimeModel/Transforms/RequestHeaderForwardedTransformTests.cs +++ b/test/ReverseProxy.Tests/Service/RuntimeModel/Transforms/RequestHeaderForwardedTransformTests.cs @@ -77,20 +77,26 @@ await transform.ApplyAsync(new RequestTransformContext() [InlineData("", "", 2, NodeFormat.IpAndPort, true, "for=\"unknown:2\"")] [InlineData("", "::1", 2, NodeFormat.Unknown, false, "for=unknown")] [InlineData("", "::1", 2, NodeFormat.UnknownAndPort, true, "for=\"unknown:2\"")] + [InlineData("", "::1", 2, NodeFormat.UnknownAndRandomPort, true, "for=\"unknown:_abcdefghi\"")] [InlineData("", "::1", 2, NodeFormat.Ip, false, "for=\"[::1]\"")] [InlineData("", "::1", 0, NodeFormat.IpAndPort, true, "for=\"[::1]\"")] [InlineData("", "::1", 2, NodeFormat.IpAndPort, true, "for=\"[::1]:2\"")] + [InlineData("", "::1", 2, NodeFormat.IpAndRandomPort, true, "for=\"[::1]:_abcdefghi\"")] [InlineData("", "127.0.0.1", 2, NodeFormat.Ip, false, "for=127.0.0.1")] [InlineData("", "127.0.0.1", 2, NodeFormat.IpAndPort, true, "for=\"127.0.0.1:2\"")] + [InlineData("", "127.0.0.1", 2, NodeFormat.IpAndRandomPort, true, "for=\"127.0.0.1:_abcdefghi\"")] [InlineData("", "::1", 2, NodeFormat.Random, false, "for=_abcdefghi")] [InlineData("", "::1", 2, NodeFormat.RandomAndPort, true, "for=\"_abcdefghi:2\"")] + [InlineData("", "::1", 2, NodeFormat.RandomAndRandomPort, true, "for=\"_abcdefghi:_jklmnopqr\"")] [InlineData("existing,header", "::1", 2, NodeFormat.Random, false, "for=_abcdefghi")] [InlineData("existing,header", "::1", 2, NodeFormat.RandomAndPort, true, "existing,header|for=\"_abcdefghi:2\"")] [InlineData("existing|header", "::1", 2, NodeFormat.RandomAndPort, true, "existing|header|for=\"_abcdefghi:2\"")] + [InlineData("existing,header", "::1", 2, NodeFormat.RandomAndRandomPort, true, "existing,header|for=\"_abcdefghi:_jklmnopqr\"")] + [InlineData("existing|header", "::1", 2, NodeFormat.RandomAndRandomPort, true, "existing|header|for=\"_abcdefghi:_jklmnopqr\"")] public async Task For_Added(string startValue, string ip, int port, NodeFormat format, bool append, string expected) { var randomFactory = new TestRandomFactory(); - randomFactory.Instance = new TestRandom() { Sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 } }; + randomFactory.Instance = new TestRandom() { Sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 } }; var httpContext = new DefaultHttpContext(); httpContext.Connection.RemoteIpAddress = string.IsNullOrEmpty(ip) ? null : IPAddress.Parse(ip); httpContext.Connection.RemotePort = port; @@ -112,22 +118,29 @@ await transform.ApplyAsync(new RequestTransformContext() [InlineData("", "", 2, NodeFormat.Ip, false, "by=unknown")] // Missing IP falls back to Unknown [InlineData("", "", 0, NodeFormat.IpAndPort, true, "by=unknown")] // Missing port excluded [InlineData("", "", 2, NodeFormat.IpAndPort, true, "by=\"unknown:2\"")] + [InlineData("", "", 2, NodeFormat.IpAndRandomPort, true, "by=\"unknown:_abcdefghi\"")] [InlineData("", "::1", 2, NodeFormat.Unknown, false, "by=unknown")] [InlineData("", "::1", 2, NodeFormat.UnknownAndPort, true, "by=\"unknown:2\"")] + [InlineData("", "::1", 2, NodeFormat.UnknownAndRandomPort, true, "by=\"unknown:_abcdefghi\"")] [InlineData("", "::1", 2, NodeFormat.Ip, false, "by=\"[::1]\"")] [InlineData("", "::1", 0, NodeFormat.IpAndPort, true, "by=\"[::1]\"")] [InlineData("", "::1", 2, NodeFormat.IpAndPort, true, "by=\"[::1]:2\"")] + [InlineData("", "::1", 2, NodeFormat.IpAndRandomPort, true, "by=\"[::1]:_abcdefghi\"")] [InlineData("", "127.0.0.1", 2, NodeFormat.Ip, false, "by=127.0.0.1")] [InlineData("", "127.0.0.1", 2, NodeFormat.IpAndPort, true, "by=\"127.0.0.1:2\"")] + [InlineData("", "127.0.0.1", 2, NodeFormat.IpAndRandomPort, true, "by=\"127.0.0.1:_abcdefghi\"")] [InlineData("", "::1", 2, NodeFormat.Random, false, "by=_abcdefghi")] [InlineData("", "::1", 2, NodeFormat.RandomAndPort, true, "by=\"_abcdefghi:2\"")] + [InlineData("", "::1", 2, NodeFormat.RandomAndRandomPort, true, "by=\"_abcdefghi:_jklmnopqr\"")] [InlineData("existing,header", "::1", 2, NodeFormat.Random, false, "by=_abcdefghi")] [InlineData("existing,header", "::1", 2, NodeFormat.RandomAndPort, true, "existing,header|by=\"_abcdefghi:2\"")] [InlineData("existing|header", "::1", 2, NodeFormat.RandomAndPort, true, "existing|header|by=\"_abcdefghi:2\"")] + [InlineData("existing,header", "::1", 2, NodeFormat.RandomAndRandomPort, true, "existing,header|by=\"_abcdefghi:_jklmnopqr\"")] + [InlineData("existing|header", "::1", 2, NodeFormat.RandomAndRandomPort, true, "existing|header|by=\"_abcdefghi:_jklmnopqr\"")] public async Task By_Added(string startValue, string ip, int port, NodeFormat format, bool append, string expected) { var randomFactory = new TestRandomFactory(); - randomFactory.Instance = new TestRandom() { Sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 } }; + randomFactory.Instance = new TestRandom() { Sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 } }; var httpContext = new DefaultHttpContext(); httpContext.Connection.LocalIpAddress = string.IsNullOrEmpty(ip) ? null : IPAddress.Parse(ip); httpContext.Connection.LocalPort = port; @@ -153,7 +166,7 @@ await transform.ApplyAsync(new RequestTransformContext() public async Task AllValues_Added(string startValue, bool append, string expected) { var randomFactory = new TestRandomFactory(); - randomFactory.Instance = new TestRandom() { Sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8 } }; + randomFactory.Instance = new TestRandom() { Sequence = new[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 } }; var httpContext = new DefaultHttpContext(); httpContext.Request.Scheme = "https"; httpContext.Request.Host = new HostString("myHost", 80); diff --git a/test/ReverseProxy.Tests/Service/SessionAffinity/AffinitizeTransformProviderTests.cs b/test/ReverseProxy.Tests/Service/SessionAffinity/AffinitizeTransformProviderTests.cs new file mode 100644 index 000000000..9df6ae357 --- /dev/null +++ b/test/ReverseProxy.Tests/Service/SessionAffinity/AffinitizeTransformProviderTests.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Linq; +using Microsoft.ReverseProxy.Abstractions; +using Microsoft.ReverseProxy.Abstractions.Config; +using Moq; +using Xunit; + +namespace Microsoft.ReverseProxy.Service.SessionAffinity +{ + public class AffinitizeTransformProviderTests + { + [Fact] + public void EnableSessionAffinity_AddsTransform() + { + var affinityProvider = new Mock(MockBehavior.Strict); + affinityProvider.SetupGet(p => p.Mode).Returns("Mode"); + + var transformProvider = new AffinitizeTransformProvider(new[] { affinityProvider.Object }); + + var cluster = new Cluster + { + Id = "cluster1", + SessionAffinity = new SessionAffinityOptions() + { + Enabled = true, + Mode = "Mode", + } + }; + + var validationContext = new TransformClusterValidationContext() + { + Cluster = cluster, + }; + transformProvider.ValidateCluster(validationContext); + + Assert.Empty(validationContext.Errors); + + var builderContext = new TransformBuilderContext() + { + Cluster = cluster, + }; + transformProvider.Apply(builderContext); + + Assert.IsType(builderContext.ResponseTransforms.Single()); + } + + [Fact] + public void EnableSession_InvalidMode_Fails() + { + var affinityProvider = new Mock(MockBehavior.Strict); + affinityProvider.SetupGet(p => p.Mode).Returns("Mode"); + + var transformProvider = new AffinitizeTransformProvider(new[] { affinityProvider.Object }); + + var cluster = new Cluster + { + Id = "cluster1", + SessionAffinity = new SessionAffinityOptions() + { + Enabled = true, + Mode = "Invalid", + } + }; + + var validationContext = new TransformClusterValidationContext() + { + Cluster = cluster, + }; + transformProvider.ValidateCluster(validationContext); + + var ex = Assert.Single(validationContext.Errors); + Assert.Equal("No matching ISessionAffinityProvider found for the session affinity mode 'Invalid' set on the cluster 'cluster1'.", ex.Message); + + var builderContext = new TransformBuilderContext() + { + Cluster = cluster, + }; + + ex = Assert.Throws(() => transformProvider.Apply(builderContext)); + Assert.Equal("No Microsoft.ReverseProxy.Service.SessionAffinity.ISessionAffinityProvider was found for the id 'Invalid'. (Parameter 'id')", ex.Message); + } + } +} diff --git a/test/ReverseProxy.Tests/Service/SessionAffinity/AffinitizeTransformTests.cs b/test/ReverseProxy.Tests/Service/SessionAffinity/AffinitizeTransformTests.cs new file mode 100644 index 000000000..ebc6b3894 --- /dev/null +++ b/test/ReverseProxy.Tests/Service/SessionAffinity/AffinitizeTransformTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.ReverseProxy.Abstractions; +using Microsoft.ReverseProxy.Middleware; +using Microsoft.ReverseProxy.RuntimeModel; +using Microsoft.ReverseProxy.Service.Management; +using Microsoft.ReverseProxy.Service.RuntimeModel.Transforms; +using Moq; +using Xunit; + +namespace Microsoft.ReverseProxy.Service.SessionAffinity +{ + public class AffinitizeTransformTests + { + [Fact] + public async Task ApplyAsync_InvokeAffinitizeRequest() + { + var cluster = GetCluster(); + var destination = cluster.DestinationManager.Items[0]; + var provider = new Mock(MockBehavior.Strict); + provider.Setup(p => p.AffinitizeRequest(It.IsAny(), It.IsNotNull(), It.IsAny())); + + var transform = new AffinitizeTransform(provider.Object); + + var context = new DefaultHttpContext(); + context.Features.Set(cluster); + + var destinationFeature = new Mock(MockBehavior.Strict); + destinationFeature.SetupProperty(p => p.ClusterConfig, cluster.Config); + destinationFeature.SetupProperty(p => p.SelectedDestination, destination); + context.Features.Set(destinationFeature.Object); + + var transformContext = new ResponseTransformContext() + { + HttpContext = context, + }; + await transform.ApplyAsync(transformContext); + + provider.Verify(); + } + + internal ClusterInfo GetCluster() + { + var destinationManager = new DestinationManager(); + destinationManager.GetOrCreateItem("dest-A", d => { }); + + var cluster = new ClusterInfo("cluster-1", destinationManager); + cluster.Config = new ClusterConfig(new Cluster + { + SessionAffinity = new SessionAffinityOptions + { + Enabled = true, + Mode = "Mode-B", + FailurePolicy = "Policy-1", + } + }, + new HttpMessageInvoker(new Mock().Object)); + + cluster.UpdateDynamicState(); + return cluster; + } + } +} diff --git a/test/ReverseProxy.Tests/Telemetry/ActivityPropagationHandlerTests.cs b/test/ReverseProxy.Tests/Telemetry/ActivityPropagationHandlerTests.cs index bee9076a1..498e75468 100644 --- a/test/ReverseProxy.Tests/Telemetry/ActivityPropagationHandlerTests.cs +++ b/test/ReverseProxy.Tests/Telemetry/ActivityPropagationHandlerTests.cs @@ -5,6 +5,7 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.ReverseProxy.Abstractions; using Microsoft.ReverseProxy.Common.Tests; using Microsoft.ReverseProxy.Telemetry; using Xunit; @@ -14,14 +15,16 @@ namespace Microsoft.ReverseProxy.Telemetry.Tests public class ActivityPropagationHandlerTests { [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task SendAsync_CurrentActivitySet_RequestHeadersSet(bool useW3CFormat) + [InlineData(false, ActivityContextHeaders.BaggageAndCorrelationContext)] + [InlineData(true, ActivityContextHeaders.BaggageAndCorrelationContext)] + [InlineData(true, ActivityContextHeaders.Baggage)] + [InlineData(true, ActivityContextHeaders.CorrelationContext)] + public async Task SendAsync_CurrentActivitySet_RequestHeadersSet(bool useW3CFormat, ActivityContextHeaders activityContextHeaders) { const string TraceStateString = "CustomTraceStateString"; string expectedId = null; - var invoker = new HttpMessageInvoker(new ActivityPropagationHandler(new MockHttpHandler( + var invoker = new HttpMessageInvoker(new ActivityPropagationHandler(activityContextHeaders, new MockHttpHandler( (HttpRequestMessage request, CancellationToken cancellationToken) => { var headers = request.Headers; @@ -35,8 +38,17 @@ public async Task SendAsync_CurrentActivitySet_RequestHeadersSet(bool useW3CForm Assert.Equal(TraceStateString, Assert.Single(values)); } - Assert.True(headers.TryGetValues("Correlation-Context", out values)); - Assert.Equal("foo=bar", Assert.Single(values)); + if (activityContextHeaders.HasFlag(ActivityContextHeaders.Baggage)) + { + Assert.True(headers.TryGetValues("Baggage", out values)); + Assert.Equal("foo=bar", Assert.Single(values)); + } + + if (activityContextHeaders.HasFlag(ActivityContextHeaders.CorrelationContext)) + { + Assert.True(headers.TryGetValues("Correlation-Context", out values)); + Assert.Equal("foo=bar", Assert.Single(values)); + } return Task.FromResult(null); }))); diff --git a/samples/BenchmarkApp/BenchmarkApp.csproj b/testassets/BenchmarkApp/BenchmarkApp.csproj similarity index 100% rename from samples/BenchmarkApp/BenchmarkApp.csproj rename to testassets/BenchmarkApp/BenchmarkApp.csproj diff --git a/samples/BenchmarkApp/Program.cs b/testassets/BenchmarkApp/Program.cs similarity index 100% rename from samples/BenchmarkApp/Program.cs rename to testassets/BenchmarkApp/Program.cs diff --git a/samples/BenchmarkApp/Properties/launchSettings.json b/testassets/BenchmarkApp/Properties/launchSettings.json similarity index 100% rename from samples/BenchmarkApp/Properties/launchSettings.json rename to testassets/BenchmarkApp/Properties/launchSettings.json diff --git a/samples/BenchmarkApp/README.md b/testassets/BenchmarkApp/README.md similarity index 100% rename from samples/BenchmarkApp/README.md rename to testassets/BenchmarkApp/README.md diff --git a/samples/BenchmarkApp/Startup.cs b/testassets/BenchmarkApp/Startup.cs similarity index 100% rename from samples/BenchmarkApp/Startup.cs rename to testassets/BenchmarkApp/Startup.cs diff --git a/testassets/BenchmarkApp/appsettings.Development.json b/testassets/BenchmarkApp/appsettings.Development.json new file mode 100644 index 000000000..34b9d2085 --- /dev/null +++ b/testassets/BenchmarkApp/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Debug", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/samples/BenchmarkApp/appsettings.json b/testassets/BenchmarkApp/appsettings.json similarity index 100% rename from samples/BenchmarkApp/appsettings.json rename to testassets/BenchmarkApp/appsettings.json diff --git a/samples/BenchmarkApp/testCert.pfx b/testassets/BenchmarkApp/testCert.pfx similarity index 100% rename from samples/BenchmarkApp/testCert.pfx rename to testassets/BenchmarkApp/testCert.pfx diff --git a/testassets/Directory.Build.props b/testassets/Directory.Build.props new file mode 100644 index 000000000..4d227585e --- /dev/null +++ b/testassets/Directory.Build.props @@ -0,0 +1,9 @@ + + + + + + + false + + \ No newline at end of file diff --git a/samples/ReverseProxy.Code.Sample/Controllers/HealthController.cs b/testassets/ReverseProxy.Code/Controllers/HealthController.cs similarity index 100% rename from samples/ReverseProxy.Code.Sample/Controllers/HealthController.cs rename to testassets/ReverseProxy.Code/Controllers/HealthController.cs diff --git a/testassets/ReverseProxy.Code/InMemoryConfigProvider.cs b/testassets/ReverseProxy.Code/InMemoryConfigProvider.cs new file mode 100644 index 000000000..a7a4c9279 --- /dev/null +++ b/testassets/ReverseProxy.Code/InMemoryConfigProvider.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading; +using Microsoft.Extensions.Primitives; +using Microsoft.ReverseProxy.Abstractions; +using Microsoft.ReverseProxy.Configuration; +using Microsoft.ReverseProxy.Service; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class InMemoryConfigProviderExtensions + { + public static IReverseProxyBuilder LoadFromMemory(this IReverseProxyBuilder builder, IReadOnlyList routes, IReadOnlyList clusters) + { + builder.Services.AddSingleton(new InMemoryConfigProvider(routes, clusters)); + return builder; + } + } +} + +namespace Microsoft.ReverseProxy.Configuration +{ + public class InMemoryConfigProvider : IProxyConfigProvider + { + private volatile InMemoryConfig _config; + + public InMemoryConfigProvider(IReadOnlyList routes, IReadOnlyList clusters) + { + _config = new InMemoryConfig(routes, clusters); + } + + public IProxyConfig GetConfig() => _config; + + public void Update(IReadOnlyList routes, IReadOnlyList clusters) + { + var oldConfig = _config; + _config = new InMemoryConfig(routes, clusters); + oldConfig.SignalChange(); + } + + private class InMemoryConfig : IProxyConfig + { + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + + public InMemoryConfig(IReadOnlyList routes, IReadOnlyList clusters) + { + Routes = routes; + Clusters = clusters; + ChangeToken = new CancellationChangeToken(_cts.Token); + } + + public IReadOnlyList Routes { get; } + + public IReadOnlyList Clusters { get; } + + public IChangeToken ChangeToken { get; } + + internal void SignalChange() + { + _cts.Cancel(); + } + } + } +} diff --git a/samples/ReverseProxy.Code.Sample/MyTransformFactory.cs b/testassets/ReverseProxy.Code/MyTransformFactory.cs similarity index 90% rename from samples/ReverseProxy.Code.Sample/MyTransformFactory.cs rename to testassets/ReverseProxy.Code/MyTransformFactory.cs index 1b5445fba..1ad1ced9f 100644 --- a/samples/ReverseProxy.Code.Sample/MyTransformFactory.cs +++ b/testassets/ReverseProxy.Code/MyTransformFactory.cs @@ -11,7 +11,7 @@ namespace Microsoft.ReverseProxy.Sample { internal class MyTransformFactory : ITransformFactory { - public bool Validate(TransformValidationContext context, IReadOnlyDictionary transformValues) + public bool Validate(TransformRouteValidationContext context, IReadOnlyDictionary transformValues) { if (transformValues.TryGetValue("CustomTransform", out var value)) { @@ -41,7 +41,7 @@ public bool Build(TransformBuilderContext context, IReadOnlyDictionary + { +#if NET + transformContext.ProxyRequest.Options.Set(new HttpRequestOptionsKey("CustomMetadata"), value); +#else + transformContext.ProxyRequest.Properties["CustomMetadata"] = value; +#endif + return default; + }); + } + } + } +} diff --git a/testassets/ReverseProxy.Code/Program.cs b/testassets/ReverseProxy.Code/Program.cs new file mode 100644 index 000000000..7c41e9acd --- /dev/null +++ b/testassets/ReverseProxy.Code/Program.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.ReverseProxy.Sample +{ + /// + /// Class that contains the entrypoint for the Reverse Proxy sample app. + /// + public class Program + { + /// + /// Entrypoint of the application. + /// + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/testassets/ReverseProxy.Code/Properties/launchSettings.json b/testassets/ReverseProxy.Code/Properties/launchSettings.json new file mode 100644 index 000000000..870f99002 --- /dev/null +++ b/testassets/ReverseProxy.Code/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "https://localhost:44356/", + "sslPort": 44356 + } + }, + "profiles": { + "ReverseProxy.Sample": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/testassets/ReverseProxy.Code/ProxyMetricsConsumer.cs b/testassets/ReverseProxy.Code/ProxyMetricsConsumer.cs new file mode 100644 index 000000000..84d952f02 --- /dev/null +++ b/testassets/ReverseProxy.Code/ProxyMetricsConsumer.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.ReverseProxy.Telemetry.Consumption; + +namespace Microsoft.ReverseProxy.Sample +{ + public sealed class ProxyMetricsConsumer : IProxyMetricsConsumer + { + public void OnProxyMetrics(ProxyMetrics oldMetrics, ProxyMetrics newMetrics) + { + var elapsed = newMetrics.Timestamp - oldMetrics.Timestamp; + var newRequests = newMetrics.RequestsStarted - oldMetrics.RequestsStarted; + Console.Title = $"Proxied {newMetrics.RequestsStarted} requests ({newRequests} in the last {(int)elapsed.TotalMilliseconds} ms)"; + } + } +} diff --git a/testassets/ReverseProxy.Code/ProxyTelemetryConsumer.cs b/testassets/ReverseProxy.Code/ProxyTelemetryConsumer.cs new file mode 100644 index 000000000..c9b036b53 --- /dev/null +++ b/testassets/ReverseProxy.Code/ProxyTelemetryConsumer.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.ReverseProxy.Service.Proxy; +using Microsoft.ReverseProxy.Telemetry.Consumption; + +namespace Microsoft.ReverseProxy.Sample +{ + public sealed class ProxyTelemetryConsumer : IProxyTelemetryConsumer + { + private readonly IHttpContextAccessor _httpContextAccessor; + + private DateTime _startTime; + + public ProxyTelemetryConsumer(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public void OnProxyStart(DateTime timestamp, string destinationPrefix) + { + _startTime = timestamp; + } + + public void OnProxyStop(DateTime timestamp, int statusCode) + { + var elapsed = timestamp - _startTime; + var path = _httpContextAccessor.HttpContext.Request.Path; + Console.WriteLine($"Spent {elapsed.TotalMilliseconds:N2} ms proxying {path}"); + } + + public void OnProxyFailed(DateTime timestamp, ProxyError error) { } + + public void OnProxyStage(DateTime timestamp, ProxyStage stage) { } + + public void OnContentTransferring(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime) { } + + public void OnContentTransferred(DateTime timestamp, bool isRequest, long contentLength, long iops, TimeSpan readTime, TimeSpan writeTime, TimeSpan firstReadTime) { } + + public void OnProxyInvoke(DateTime timestamp, string clusterId, string routeId, string destinationId) { } + } +} diff --git a/testassets/ReverseProxy.Code/ReverseProxy.Code.csproj b/testassets/ReverseProxy.Code/ReverseProxy.Code.csproj new file mode 100644 index 000000000..a1032320a --- /dev/null +++ b/testassets/ReverseProxy.Code/ReverseProxy.Code.csproj @@ -0,0 +1,18 @@ + + + + net5.0;netcoreapp3.1 + Exe + Microsoft.ReverseProxy.Sample + + + + + + + + + + + + diff --git a/testassets/ReverseProxy.Code/Startup.cs b/testassets/ReverseProxy.Code/Startup.cs new file mode 100644 index 000000000..47fc5979b --- /dev/null +++ b/testassets/ReverseProxy.Code/Startup.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net.Http.Headers; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.ReverseProxy.Abstractions; +using Microsoft.ReverseProxy.Abstractions.Config; +using Microsoft.ReverseProxy.Middleware; +using Microsoft.ReverseProxy.Telemetry.Consumption; + +namespace Microsoft.ReverseProxy.Sample +{ + /// + /// ASP .NET Core pipeline initialization. + /// + public class Startup + { + /// + /// This method gets called by the runtime. Use this method to add services to the container. + /// + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + var routes = new[] + { + new ProxyRoute() + { + RouteId = "route1", + ClusterId = "cluster1", + Match = new ProxyMatch + { + Path = "{**catch-all}" + } + } + }; + var clusters = new[] + { + new Cluster() + { + Id = "cluster1", + SessionAffinity = new SessionAffinityOptions { Enabled = true, Mode = "Cookie" }, + Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "destination1", new Destination() { Address = "https://localhost:10000" } } + } + } + }; + + services.AddReverseProxy() + .LoadFromMemory(routes, clusters) + .AddTransformFactory() + .AddTransforms() + .AddTransforms(transformBuilderContext => + { + // For each route+cluster pair decide if we want to add transforms, and if so, which? + // This logic is re-run each time a route is rebuilt. + + transformBuilderContext.AddPathPrefix("/prefix"); + + // Only do this for routes that require auth. + if (string.Equals("token", transformBuilderContext.Route.AuthorizationPolicy)) + { + transformBuilderContext.AddRequestTransform(async transformContext => + { + // AuthN and AuthZ will have already been completed after request routing. + var ticket = await transformContext.HttpContext.AuthenticateAsync("token"); + var tokenService = transformContext.HttpContext.RequestServices.GetRequiredService(); + var token = await tokenService.GetAuthTokenAsync(ticket.Principal); + transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + }); + } + }); + + services.AddHttpContextAccessor(); + services.AddSingleton(); + services.AddScoped(); + services.AddProxyTelemetryListener(); + } + + /// + /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapReverseProxy(proxyPipeline => + { + // Custom endpoint selection + proxyPipeline.Use((context, next) => + { + var someCriteria = false; // MeetsCriteria(context); + if (someCriteria) + { + var availableDestinationsFeature = context.Features.Get(); + var destination = availableDestinationsFeature.AvailableDestinations[0]; // PickDestination(availableDestinationsFeature.Destinations); + // Load balancing will no-op if we've already reduced the list of available destinations to 1. + availableDestinationsFeature.AvailableDestinations = destination; + } + + return next(); + }); + proxyPipeline.UseAffinitizedDestinationLookup(); + proxyPipeline.UseProxyLoadBalancing(); + }); + }); + } + } +} diff --git a/testassets/ReverseProxy.Code/TokenService.cs b/testassets/ReverseProxy.Code/TokenService.cs new file mode 100644 index 000000000..92109a2ea --- /dev/null +++ b/testassets/ReverseProxy.Code/TokenService.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.ReverseProxy.Sample +{ + internal class TokenService + { + internal Task GetAuthTokenAsync(ClaimsPrincipal user) + { + return Task.FromResult(user.Identity.Name); + } + } +} diff --git a/testassets/ReverseProxy.Code/appsettings.Development.json b/testassets/ReverseProxy.Code/appsettings.Development.json new file mode 100644 index 000000000..34b9d2085 --- /dev/null +++ b/testassets/ReverseProxy.Code/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Debug", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/testassets/ReverseProxy.Code/appsettings.json b/testassets/ReverseProxy.Code/appsettings.json new file mode 100644 index 000000000..d9d9a9bff --- /dev/null +++ b/testassets/ReverseProxy.Code/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/ReverseProxy.Config.Sample/Controllers/HealthController.cs b/testassets/ReverseProxy.Config/Controllers/HealthController.cs similarity index 100% rename from samples/ReverseProxy.Config.Sample/Controllers/HealthController.cs rename to testassets/ReverseProxy.Config/Controllers/HealthController.cs diff --git a/testassets/ReverseProxy.Config/CustomConfigFilter.cs b/testassets/ReverseProxy.Config/CustomConfigFilter.cs new file mode 100644 index 000000000..ca991bc10 --- /dev/null +++ b/testassets/ReverseProxy.Config/CustomConfigFilter.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.ReverseProxy.Abstractions; +using Microsoft.ReverseProxy.Service; + +namespace Microsoft.ReverseProxy.Sample +{ + public class CustomConfigFilter : IProxyConfigFilter + { + public ValueTask ConfigureClusterAsync(Cluster cluster, CancellationToken cancel) + { + // How to use custom metadata to configure clusters + if (cluster.Metadata?.TryGetValue("CustomHealth", out var customHealth) ?? false + && string.Equals(customHealth, "true", StringComparison.OrdinalIgnoreCase)) + { + cluster = cluster with + { + HealthCheck = new HealthCheckOptions + { + Active = new ActiveHealthCheckOptions + { + Enabled = true, + Policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures, + }, + Passive = cluster.HealthCheck?.Passive, + } + }; + } + + // Or wrap the meatadata in config sugar + var config = new ConfigurationBuilder().AddInMemoryCollection(cluster.Metadata).Build(); + if (config.GetValue("CustomHealth")) + { + cluster = cluster with + { + HealthCheck = new HealthCheckOptions + { + Active = new ActiveHealthCheckOptions + { + Enabled = true, + Policy = HealthCheckConstants.ActivePolicy.ConsecutiveFailures, + }, + Passive = cluster.HealthCheck?.Passive, + } + }; + } + + return new ValueTask(cluster); + } + + public ValueTask ConfigureRouteAsync(ProxyRoute route, CancellationToken cancel) + { + // Do not let config based routes take priority over code based routes. + // Lower numbers are higher priority. Code routes default to 0. + if (route.Order.HasValue && route.Order.Value < 1) + { + return new ValueTask(route with { Order = 1 }); + } + + return new ValueTask(route); + } + } +} diff --git a/testassets/ReverseProxy.Config/Program.cs b/testassets/ReverseProxy.Config/Program.cs new file mode 100644 index 000000000..7c41e9acd --- /dev/null +++ b/testassets/ReverseProxy.Config/Program.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.ReverseProxy.Sample +{ + /// + /// Class that contains the entrypoint for the Reverse Proxy sample app. + /// + public class Program + { + /// + /// Entrypoint of the application. + /// + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/testassets/ReverseProxy.Config/Properties/launchSettings.json b/testassets/ReverseProxy.Config/Properties/launchSettings.json new file mode 100644 index 000000000..870f99002 --- /dev/null +++ b/testassets/ReverseProxy.Config/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "https://localhost:44356/", + "sslPort": 44356 + } + }, + "profiles": { + "ReverseProxy.Sample": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/testassets/ReverseProxy.Config/ReverseProxy.Config.csproj b/testassets/ReverseProxy.Config/ReverseProxy.Config.csproj new file mode 100644 index 000000000..abaec5790 --- /dev/null +++ b/testassets/ReverseProxy.Config/ReverseProxy.Config.csproj @@ -0,0 +1,17 @@ + + + + net5.0;netcoreapp3.1 + Exe + Microsoft.ReverseProxy.Sample + + + + + + + + + + + diff --git a/testassets/ReverseProxy.Config/Startup.cs b/testassets/ReverseProxy.Config/Startup.cs new file mode 100644 index 000000000..5b886872c --- /dev/null +++ b/testassets/ReverseProxy.Config/Startup.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.ReverseProxy.Middleware; + +namespace Microsoft.ReverseProxy.Sample +{ + /// + /// ASP .NET Core pipeline initialization. + /// + public class Startup + { + private readonly IConfiguration _configuration; + + /// + /// Initializes a new instance of the class. + /// + public Startup(IConfiguration configuration) + { + _configuration = configuration; + } + + /// + /// This method gets called by the runtime. Use this method to add services to the container. + /// + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddReverseProxy() + .LoadFromConfig(_configuration.GetSection("ReverseProxy")) + .AddProxyConfigFilter(); + } + + /// + /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseAuthorization(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapReverseProxy(proxyPipeline => + { + // Custom endpoint selection + proxyPipeline.Use((context, next) => + { + var someCriteria = false; // MeetsCriteria(context); + if (someCriteria) + { + var availableDestinationsFeature = context.Features.Get(); + var destination = availableDestinationsFeature.AvailableDestinations[0]; // PickDestination(availableDestinationsFeature.Destinations); + // Load balancing will no-op if we've already reduced the list of available destinations to 1. + availableDestinationsFeature.AvailableDestinations = destination; + } + + return next(); + }); + proxyPipeline.UseAffinitizedDestinationLookup(); + proxyPipeline.UseProxyLoadBalancing(); + proxyPipeline.UsePassiveHealthChecks(); + }) + .ConfigureEndpoints((builder, route) => builder.WithDisplayName($"ReverseProxy {route.RouteId}-{route.ClusterId}")); + }); + } + } +} diff --git a/testassets/ReverseProxy.Config/appsettings.Development.json b/testassets/ReverseProxy.Config/appsettings.Development.json new file mode 100644 index 000000000..34b9d2085 --- /dev/null +++ b/testassets/ReverseProxy.Config/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Debug", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/testassets/ReverseProxy.Config/appsettings.json b/testassets/ReverseProxy.Config/appsettings.json new file mode 100644 index 000000000..2a4338680 --- /dev/null +++ b/testassets/ReverseProxy.Config/appsettings.json @@ -0,0 +1,130 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "Endpoints": { + "https": { + "url": "https://localhost:5001" + }, + "http": { + "url": "http://localhost:5000" + } + } + }, + "ReverseProxy": { + "Clusters": { + "cluster1": { + "LoadBalancingPolicy": "Random", + "SessionAffinity": { + "Enabled": "true", + "Mode": "Cookie" + }, + "HealthCheck": { + "Active": { + "Enabled": "true", + "Interval": "00:00:10", + "Timeout": "00:00:10", + "Policy": "ConsecutiveFailures", + "Path": "/api/health" + }, + "Passive": { + "Enabled": "true", + "Policy": "TransportFailureRate", + "ReactivationPeriod": "00:05:00" + } + }, + "Metadata": { + "ConsecutiveFailuresHealthPolicy.Threshold": "3", + "TransportFailureRateHealthPolicy.RateLimit": "0.5" + }, + "Destinations": { + "cluster1/destination1": { + "Address": "https://localhost:10000/" + }, + "cluster1/destination2": { + "Address": "http://localhost:10010/" + } + } + }, + "cluster2": { + "Metadata": { + "CustomHealth": true + }, + "Destinations": { + "cluster2/destination1": { + "Address": "https://localhost:10001/", + "Health": "https://localhost:10001/api/health" + } + } + } + }, + "Routes": [ + { + "RouteId": "route1", + "ClusterId": "cluster1", + "Match": { + "Methods": [ "GET", "POST" ], + "Hosts": [ "localhost" ], + "Path": "/api/{action}" + } + }, + { + "RouteId": "route2", + "ClusterId": "cluster2", + "Match": { + "Hosts": [ "localhost" ], + "Path": "/api/{plugin}/stuff/{*remainder}" + }, + "Transforms": [ + { "PathPattern": "/foo/{plugin}/bar/{remainder}" }, + { + "X-Forwarded": "proto,host,for,pathbase", + "Append": "true", + "Prefix": "X-Forwarded-" + }, + { + "Forwarded": "by,host,for,proto", + "ByFormat": "Random", + "ForFormat": "IpAndPort" + }, + { "ClientCert": "X-Client-Cert" }, + + { "PathSet": "/apis" }, + { "PathPrefix": "/apis" }, + { "PathRemovePrefix": "/apis" }, + + { "RequestHeadersCopy": "true" }, + { "RequestHeaderOriginalHost": "true" }, + { + "RequestHeader": "foo0", + "Append": "bar" + }, + { + "RequestHeader": "foo1", + "Set": "bar, baz" + }, + { + "RequestHeader": "clearMe", + "Set": "" + }, + { + "ResponseHeader": "foo", + "Append": "bar", + "When": "Always" + }, + { + "ResponseTrailer": "foo", + "Append": "trailer", + "When": "Always" + } + ] + } + ] + } +} diff --git a/samples/SampleClient/CommandLineArgs.cs b/testassets/TestClient/CommandLineArgs.cs similarity index 100% rename from samples/SampleClient/CommandLineArgs.cs rename to testassets/TestClient/CommandLineArgs.cs diff --git a/samples/SampleClient/Program.cs b/testassets/TestClient/Program.cs similarity index 100% rename from samples/SampleClient/Program.cs rename to testassets/TestClient/Program.cs diff --git a/samples/SampleClient/Properties/launchSettings.json b/testassets/TestClient/Properties/launchSettings.json similarity index 100% rename from samples/SampleClient/Properties/launchSettings.json rename to testassets/TestClient/Properties/launchSettings.json diff --git a/samples/SampleClient/Scenarios/Http1Scenario.cs b/testassets/TestClient/Scenarios/Http1Scenario.cs similarity index 100% rename from samples/SampleClient/Scenarios/Http1Scenario.cs rename to testassets/TestClient/Scenarios/Http1Scenario.cs diff --git a/samples/SampleClient/Scenarios/Http2Scenario.cs b/testassets/TestClient/Scenarios/Http2Scenario.cs similarity index 100% rename from samples/SampleClient/Scenarios/Http2Scenario.cs rename to testassets/TestClient/Scenarios/Http2Scenario.cs diff --git a/samples/SampleClient/Scenarios/IScenario.cs b/testassets/TestClient/Scenarios/IScenario.cs similarity index 100% rename from samples/SampleClient/Scenarios/IScenario.cs rename to testassets/TestClient/Scenarios/IScenario.cs diff --git a/samples/SampleClient/Scenarios/RawUpgradeScenario.cs b/testassets/TestClient/Scenarios/RawUpgradeScenario.cs similarity index 100% rename from samples/SampleClient/Scenarios/RawUpgradeScenario.cs rename to testassets/TestClient/Scenarios/RawUpgradeScenario.cs diff --git a/samples/SampleClient/Scenarios/SessionAffinityScenario.cs b/testassets/TestClient/Scenarios/SessionAffinityScenario.cs similarity index 100% rename from samples/SampleClient/Scenarios/SessionAffinityScenario.cs rename to testassets/TestClient/Scenarios/SessionAffinityScenario.cs diff --git a/samples/SampleClient/Scenarios/WebSocketsScenario.cs b/testassets/TestClient/Scenarios/WebSocketsScenario.cs similarity index 100% rename from samples/SampleClient/Scenarios/WebSocketsScenario.cs rename to testassets/TestClient/Scenarios/WebSocketsScenario.cs diff --git a/samples/SampleClient/SampleClient.csproj b/testassets/TestClient/TestClient.csproj similarity index 100% rename from samples/SampleClient/SampleClient.csproj rename to testassets/TestClient/TestClient.csproj diff --git a/samples/SampleServer/AssemblyInfo.cs b/testassets/TestServer/AssemblyInfo.cs similarity index 100% rename from samples/SampleServer/AssemblyInfo.cs rename to testassets/TestServer/AssemblyInfo.cs diff --git a/samples/ReverseProxy.Direct.Sample/Controllers/HealthController.cs b/testassets/TestServer/Controllers/HealthController.cs similarity index 61% rename from samples/ReverseProxy.Direct.Sample/Controllers/HealthController.cs rename to testassets/TestServer/Controllers/HealthController.cs index a5e9bdc00..a683a0ed4 100644 --- a/samples/ReverseProxy.Direct.Sample/Controllers/HealthController.cs +++ b/testassets/TestServer/Controllers/HealthController.cs @@ -6,20 +6,22 @@ namespace Microsoft.ReverseProxy.Sample.Controllers { /// - /// Controller for health check api. + /// Controller for active health check probes. /// [ApiController] public class HealthController : ControllerBase { + private static volatile int _count; /// - /// Returns 200 if Proxy is healthy. + /// Returns 200 if server is healthy. /// [HttpGet] [Route("/api/health")] public IActionResult CheckHealth() { - // TODO: Implement health controller, use guid in route. - return Ok(); + _count++; + // Simulate temporary health degradation. + return _count % 10 < 4 ? Ok() : StatusCode(500); } } } diff --git a/testassets/TestServer/Controllers/HttpController.cs b/testassets/TestServer/Controllers/HttpController.cs new file mode 100644 index 000000000..575f662fd --- /dev/null +++ b/testassets/TestServer/Controllers/HttpController.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace SampleServer.Controllers +{ + /// + /// Sample controller. + /// + [ApiController] + public class HttpController : ControllerBase + { + /// + /// Returns a 200 response. + /// + [HttpGet] + [Route("/api/noop")] + public void NoOp() + { + } + + /// + /// Returns a 200 response dumping all info from the incoming request. + /// + [HttpGet, HttpPost] + [Route("/api/dump")] + [Route("/{**catchall}", Order = int.MaxValue)] // Make this the default route if nothing matches + public async Task Dump() + { + var result = new { + Request.Protocol, + Request.Method, + Request.Scheme, + Host = Request.Host.Value, + PathBase = Request.PathBase.Value, + Path = Request.Path.Value, + Query = Request.QueryString.Value, + Headers = Request.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()), + Time = DateTimeOffset.UtcNow, + Body = await new StreamReader(Request.Body).ReadToEndAsync(), + }; + + return Ok(result); + } + + /// + /// Returns a 200 response dumping all info from the incoming request. + /// + [HttpGet] + [Route("/api/statuscode")] + public void Status(int statusCode) + { + Response.StatusCode = statusCode; + } + + /// + /// Returns a 200 response dumping all info from the incoming request. + /// + [HttpGet] + [Route("/api/headers")] + public void Headers([FromBody] Dictionary headers) + { + foreach (var (key, value) in headers) + { + Response.Headers.Add(key, value); + } + } + + /// + /// Returns a 200 response after milliseconds + /// and containing with bytes in the response body. + /// + [HttpGet] + [HttpPut] + [HttpPost] + [HttpPatch] + [Route("/api/stress")] + public async Task Stress([FromQuery] int delay, [FromQuery] int responseSize) + { + var bodyReader = Request.BodyReader; + if (bodyReader != null) + { + while (true) + { + var a = await Request.BodyReader.ReadAsync(); + if (a.IsCompleted) + { + break; + } + } + } + + if (delay > 0) + { + await Task.Delay(delay); + } + + var bodyWriter = Response.BodyWriter; + if (bodyWriter != null && responseSize > 0) + { + const int WriteBufferSize = 4096; + + var remaining = responseSize; + var buffer = new byte[WriteBufferSize]; + + while (remaining > 0) + { + buffer[0] = (byte)(remaining * 17); // Make the output not all zeros + var toWrite = Math.Min(buffer.Length, remaining); + await bodyWriter.WriteAsync(new ReadOnlyMemory(buffer, 0, toWrite), + HttpContext.RequestAborted); + remaining -= toWrite; + } + } + } + } +} diff --git a/samples/SampleServer/Controllers/UpgradeController.cs b/testassets/TestServer/Controllers/UpgradeController.cs similarity index 100% rename from samples/SampleServer/Controllers/UpgradeController.cs rename to testassets/TestServer/Controllers/UpgradeController.cs diff --git a/testassets/TestServer/Controllers/WebSocketsController.cs b/testassets/TestServer/Controllers/WebSocketsController.cs new file mode 100644 index 000000000..12009c444 --- /dev/null +++ b/testassets/TestServer/Controllers/WebSocketsController.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Net.WebSockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace SampleServer.Controllers +{ + /// + /// Sample controller. + /// + [ApiController] + public class WebSocketsController : ControllerBase + { + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + public WebSocketsController(ILogger logger) + { + _logger = logger; + } + + /// + /// Returns a 200 response. + /// + [HttpGet] + [Route("/api/websockets")] + public async Task WebSockets() + { + if (!HttpContext.WebSockets.IsWebSocketRequest) + { + HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; + } + + using (var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync()) + { + _logger.LogInformation("WebSockets established."); + await RunPingPongAsync(webSocket, HttpContext.RequestAborted); + } + + _logger.LogInformation("WebSockets finished."); + } + + private static async Task RunPingPongAsync(WebSocket webSocket, CancellationToken cancellation) + { + var buffer = new byte[1024]; + while (true) + { + var message = await webSocket.ReceiveAsync(buffer, cancellation); + if (message.MessageType == WebSocketMessageType.Close) + { + await webSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Bye", cancellation); + return; + } + + await webSocket.SendAsync(new ArraySegment(buffer, 0, message.Count), + message.MessageType, + message.EndOfMessage, + cancellation); + } + } + } +} diff --git a/testassets/TestServer/Program.cs b/testassets/TestServer/Program.cs new file mode 100644 index 000000000..855fb4084 --- /dev/null +++ b/testassets/TestServer/Program.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace SampleServer +{ + /// + /// Class that contains the entrypoint for the Reverse Proxy sample app. + /// + public class Program + { + /// + /// Entrypoint of the application. + /// + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + private static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/testassets/TestServer/Properties/launchSettings.json b/testassets/TestServer/Properties/launchSettings.json new file mode 100644 index 000000000..10caee066 --- /dev/null +++ b/testassets/TestServer/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ReverseProxy.Sample": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:10000;https://localhost:10001;http://localhost:10010;http://localhost:10011" + } + } +} diff --git a/testassets/TestServer/Startup.cs b/testassets/TestServer/Startup.cs new file mode 100644 index 000000000..8615286af --- /dev/null +++ b/testassets/TestServer/Startup.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace SampleServer +{ + /// + /// ASP .NET Core pipeline initialization. + /// + public class Startup + { + /// + /// This method gets called by the runtime. Use this method to add services to the container. + /// + public void ConfigureServices(IServiceCollection services) + { + services + .AddControllers() + .AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true); + } + + /// + /// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + public void Configure(IApplicationBuilder app) + { + // Disabling https redirection behind the proxy. Forwarders are not currently set up so we can't tell if the external connection used https. + // Nor do we know the correct port to redirect to. + // app.UseHttpsRedirection(); + + app.UseWebSockets(); + + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/testassets/TestServer/TestServer.csproj b/testassets/TestServer/TestServer.csproj new file mode 100644 index 000000000..ee7abf0d4 --- /dev/null +++ b/testassets/TestServer/TestServer.csproj @@ -0,0 +1,13 @@ + + + + net5.0 + Exe + SampleServer + + + + + + + diff --git a/testassets/TestServer/appsettings.Development.json b/testassets/TestServer/appsettings.Development.json new file mode 100644 index 000000000..63ed6e146 --- /dev/null +++ b/testassets/TestServer/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/testassets/TestServer/appsettings.json b/testassets/TestServer/appsettings.json new file mode 100644 index 000000000..222224e36 --- /dev/null +++ b/testassets/TestServer/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file